Merge branch 'jt/push-options-doc'
authorJunio C Hamano <gitster@pobox.com>
Tue, 23 May 2017 04:46:07 +0000 (13:46 +0900)
committerJunio C Hamano <gitster@pobox.com>
Tue, 23 May 2017 04:46:07 +0000 (13:46 +0900)
The receive-pack program now makes sure that the push certificate
records the same set of push options used for pushing.

* jt/push-options-doc:
receive-pack: verify push options in cert
docs: correct receive.advertisePushOptions default

Documentation/config.txt
Documentation/technical/pack-protocol.txt
builtin/receive-pack.c
t/t5534-push-signed.sh
index 96e9cf8b730cd0ab0b2152180a92cad32c7ca8e9..e0b9fd0bc38a08077896240384f0bb1690532884 100644 (file)
@@ -2623,9 +2623,8 @@ receive.advertiseAtomic::
        capability, set this variable to false.
 
 receive.advertisePushOptions::
-       By default, git-receive-pack will advertise the push options
-       capability to its clients. If you don't want to advertise this
-       capability, set this variable to false.
+       When set to true, git-receive-pack will advertise the push options
+       capability to its clients. False by default.
 
 receive.autogc::
        By default, git-receive-pack will run "git-gc --auto" after
index 5b0ba3ef20144d1676cedbd8435e7bffddcde2f7..a34917153fd3dc10fefe71a1ba05e5dacb9af7f9 100644 (file)
@@ -473,13 +473,10 @@ that it wants to update, it sends a line listing the obj-id currently on
 the server, the obj-id the client would like to update it to and the name
 of the reference.
 
-This list is followed by a flush-pkt. Then the push options are transmitted
-one per packet followed by another flush-pkt. After that the packfile that
-should contain all the objects that the server will need to complete the new
-references will be sent.
+This list is followed by a flush-pkt.
 
 ----
-  update-request    =  *shallow ( command-list | push-cert ) [packfile]
+  update-requests   =  *shallow ( command-list | push-cert )
 
   shallow           =  PKT-LINE("shallow" SP obj-id)
 
@@ -500,12 +497,35 @@ references will be sent.
                      PKT-LINE("pusher" SP ident LF)
                      PKT-LINE("pushee" SP url LF)
                      PKT-LINE("nonce" SP nonce LF)
+                     *PKT-LINE("push-option" SP push-option LF)
                      PKT-LINE(LF)
                      *PKT-LINE(command LF)
                      *PKT-LINE(gpg-signature-lines LF)
                      PKT-LINE("push-cert-end" LF)
 
-  packfile          = "PACK" 28*(OCTET)
+  push-option       =  1*( VCHAR | SP )
+----
+
+If the server has advertised the 'push-options' capability and the client has
+specified 'push-options' as part of the capability list above, the client then
+sends its push options followed by a flush-pkt.
+
+----
+  push-options      =  *PKT-LINE(push-option) flush-pkt
+----
+
+For backwards compatibility with older Git servers, if the client sends a push
+cert and push options, it MUST send its push options both embedded within the
+push cert and after the push cert. (Note that the push options within the cert
+are prefixed, but the push options after the cert are not.) Both these lists
+MUST be the same, modulo the prefix.
+
+After that the packfile that
+should contain all the objects that the server will need to complete the new
+references will be sent.
+
+----
+  packfile          =  "PACK" 28*(OCTET)
 ----
 
 If the receiving end does not support delete-refs, the sending end MUST
index 0bb36d584d7e97ef0234c932d27e8312c1461107..1b29975c528167ac5284b5710afcbd8c395a9c0a 100644 (file)
@@ -473,7 +473,8 @@ static char *prepare_push_cert_nonce(const char *path, timestamp_t stamp)
  * after dropping "_commit" from its name and possibly moving it out
  * of commit.c
  */
-static char *find_header(const char *msg, size_t len, const char *key)
+static char *find_header(const char *msg, size_t len, const char *key,
+                        const char **next_line)
 {
        int key_len = strlen(key);
        const char *line = msg;
@@ -486,6 +487,8 @@ static char *find_header(const char *msg, size_t len, const char *key)
                if (line + key_len < eol &&
                    !memcmp(line, key, key_len) && line[key_len] == ' ') {
                        int offset = key_len + 1;
+                       if (next_line)
+                               *next_line = *eol ? eol + 1 : eol;
                        return xmemdupz(line + offset, (eol - line) - offset);
                }
                line = *eol ? eol + 1 : NULL;
@@ -495,7 +498,7 @@ static char *find_header(const char *msg, size_t len, const char *key)
 
 static const char *check_nonce(const char *buf, size_t len)
 {
-       char *nonce = find_header(buf, len, "nonce");
+       char *nonce = find_header(buf, len, "nonce", NULL);
        timestamp_t stamp, ostamp;
        char *bohmac, *expect = NULL;
        const char *retval = NONCE_BAD;
@@ -575,6 +578,45 @@ static const char *check_nonce(const char *buf, size_t len)
        return retval;
 }
 
+/*
+ * Return 1 if there is no push_cert or if the push options in push_cert are
+ * the same as those in the argument; 0 otherwise.
+ */
+static int check_cert_push_options(const struct string_list *push_options)
+{
+       const char *buf = push_cert.buf;
+       int len = push_cert.len;
+
+       char *option;
+       const char *next_line;
+       int options_seen = 0;
+
+       int retval = 1;
+
+       if (!len)
+               return 1;
+
+       while ((option = find_header(buf, len, "push-option", &next_line))) {
+               len -= (next_line - buf);
+               buf = next_line;
+               options_seen++;
+               if (options_seen > push_options->nr
+                   || strcmp(option,
+                             push_options->items[options_seen - 1].string)) {
+                       retval = 0;
+                       goto leave;
+               }
+               free(option);
+       }
+
+       if (options_seen != push_options->nr)
+               retval = 0;
+
+leave:
+       free(option);
+       return retval;
+}
+
 static void prepare_push_cert_sha1(struct child_process *proc)
 {
        static int already_done;
@@ -1929,6 +1971,11 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)
 
                if (use_push_options)
                        read_push_options(&push_options);
+               if (!check_cert_push_options(&push_options)) {
+                       struct command *cmd;
+                       for (cmd = commands; cmd; cmd = cmd->next)
+                               cmd->error_string = "inconsistent push options";
+               }
 
                prepare_shallow_info(&si, &shallow);
                if (!si.nr_ours && !si.nr_theirs)
index ecb8d446a58d422bd8a9a25f75e4f9a20ec50818..5bcb288f5c4d02e7994d745bc07485bf1bf6c116 100755 (executable)
@@ -124,6 +124,43 @@ test_expect_success GPG 'signed push sends push certificate' '
        test_cmp expect dst/push-cert-status
 '
 
+test_expect_success GPG 'inconsistent push options in signed push not allowed' '
+       # First, invoke receive-pack with dummy input to obtain its preamble.
+       prepare_dst &&
+       git -C dst config receive.certnonceseed sekrit &&
+       git -C dst config receive.advertisepushoptions 1 &&
+       printf xxxx | test_might_fail git receive-pack dst >preamble &&
+
+       # Then, invoke push. Simulate a receive-pack that sends the preamble we
+       # obtained, followed by a dummy packet.
+       write_script myscript <<-\EOF &&
+               cat preamble &&
+               printf xxxx &&
+               cat >push
+       EOF
+       test_might_fail git push --push-option="foo" --push-option="bar" \
+               --receive-pack="\"$(pwd)/myscript\"" --signed dst --delete ff &&
+
+       # Replay the push output on a fresh dst, checking that ff is truly
+       # deleted.
+       prepare_dst &&
+       git -C dst config receive.certnonceseed sekrit &&
+       git -C dst config receive.advertisepushoptions 1 &&
+       git receive-pack dst <push &&
+       test_must_fail git -C dst rev-parse ff &&
+
+       # Tweak the push output to make the push option outside the cert
+       # different, then replay it on a fresh dst, checking that ff is not
+       # deleted.
+       perl -pe "s/([^ ])bar/\$1baz/" push >push.tweak &&
+       prepare_dst &&
+       git -C dst config receive.certnonceseed sekrit &&
+       git -C dst config receive.advertisepushoptions 1 &&
+       git receive-pack dst <push.tweak >out &&
+       git -C dst rev-parse ff &&
+       grep "inconsistent push options" out
+'
+
 test_expect_success GPG 'fail without key and heed user.signingkey' '
        prepare_dst &&
        mkdir -p dst/.git/hooks &&