http-backend: respect CONTENT_LENGTH for receive-pack

Push passes to another commands, as described in https://public-inbox.org/git/20171129032214.GB32345@sigill.intra.peff.net/ As it gets complicated to correctly track the data length, instead transfer the data through parent process and cut the pipe as the specified length is reached. Do it only when CONTENT_LENGTH is set, otherwise pass the input directly to the forked commands. Add tests for cases: * CONTENT_LENGTH is set, script's stdin has more data, with all combinations of variations: fetch or push, plain or compressed body, correct or truncated input. * CONTENT_LENGTH is specified to a value which does not fit into ssize_t. Helped-by: Junio C Hamano <gitster@pobox.com> Signed-off-by: Max Kirillov <max@max630.net> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Max Kirillov committed Jul 27, 2018 at 06:48 UTC 6c213e863aeb0af078bf82deefb22da20427c2ab
4 files changed +223 -2
help.c
+1
@@ -609,6 +609,7 @@ int cmd_version(int argc, const char **argv, const char *prefix)
609 else
610 printf("no commit associated with this build\n");
611 printf("sizeof-long: %d\n", (int)sizeof(long));
612 + printf("sizeof-size_t: %d\n", (int)sizeof(size_t));
613 /* NEEDSWORK: also save and output GIT-BUILD_OPTIONS? */
614 }
615 return 0;
http-backend.c
+30 -2
@@ -373,6 +373,8 @@ static void inflate_request(const char *prog_name, int out, int buffer_input, ss
373 unsigned char in_buf[8192];
374 unsigned char out_buf[8192];
375 unsigned long cnt = 0;
376 + int req_len_defined = req_len >= 0;
377 + size_t req_remaining_len = req_len;
378
379 memset(&stream, 0, sizeof(stream));
380 git_inflate_init_gzip_only(&stream);
@@ -387,8 +389,15 @@ static void inflate_request(const char *prog_name, int out, int buffer_input, ss
389 n = read_request(0, &full_request, req_len);
390 stream.next_in = full_request;
391 } else {
390 - n = xread(0, in_buf, sizeof(in_buf));
392 + ssize_t buffer_len;
393 + if (req_len_defined && req_remaining_len <= sizeof(in_buf))
394 + buffer_len = req_remaining_len;
395 + else
396 + buffer_len = sizeof(in_buf);
397 + n = xread(0, in_buf, buffer_len);
398 stream.next_in = in_buf;
399 + if (req_len_defined && n > 0)
400 + req_remaining_len -= n;
401 }
402
403 if (n <= 0)
@@ -431,6 +440,23 @@ static void copy_request(const char *prog_name, int out, ssize_t req_len)
440 free(buf);
441 }
442
443 +static void pipe_fixed_length(const char *prog_name, int out, size_t req_len)
444 +{
445 + unsigned char buf[8192];
446 + size_t remaining_len = req_len;
447 +
448 + while (remaining_len > 0) {
449 + size_t chunk_length = remaining_len > sizeof(buf) ? sizeof(buf) : remaining_len;
450 + ssize_t n = xread(0, buf, chunk_length);
451 + if (n < 0)
452 + die_errno("Reading request failed");
453 + write_to_child(out, buf, n, prog_name);
454 + remaining_len -= n;
455 + }
456 +
457 + close(out);
458 +}
459 +
460 static void run_service(const char **argv, int buffer_input)
461 {
462 const char *encoding = getenv("HTTP_CONTENT_ENCODING");
@@ -457,7 +483,7 @@ static void run_service(const char **argv, int buffer_input)
483 "GIT_COMMITTER_EMAIL=%s@http.%s", user, host);
484
485 cld.argv = argv;
460 - if (buffer_input || gzipped_request)
486 + if (buffer_input || gzipped_request || req_len >= 0)
487 cld.in = -1;
488 cld.git_cmd = 1;
489 if (start_command(&cld))
@@ -468,6 +494,8 @@ static void run_service(const char **argv, int buffer_input)
494 inflate_request(argv[0], cld.in, buffer_input, req_len);
495 else if (buffer_input)
496 copy_request(argv[0], cld.in, req_len);
497 + else if (req_len >= 0)
498 + pipe_fixed_length(argv[0], cld.in, req_len);
499 else
500 close(0);
501
t/t5562-http-backend-content-length.sh new
+155
@@ -0,0 +1,155 @@
1 +#!/bin/sh
2 +
3 +test_description='test git-http-backend respects CONTENT_LENGTH'
4 +. ./test-lib.sh
5 +
6 +test_lazy_prereq GZIP 'gzip --version'
7 +
8 +verify_http_result() {
9 + # some fatal errors still produce status 200
10 + # so check if there is the error message
11 + if grep 'fatal:' act.err
12 + then
13 + return 1
14 + fi
15 +
16 + if ! grep "Status" act.out >act
17 + then
18 + printf "Status: 200 OK\r\n" >act
19 + fi
20 + printf "Status: $1\r\n" >exp &&
21 + test_cmp exp act
22 +}
23 +
24 +test_http_env() {
25 + handler_type="$1"
26 + request_body="$2"
27 + shift
28 + env \
29 + CONTENT_TYPE="application/x-git-$handler_type-pack-request" \
30 + QUERY_STRING="/repo.git/git-$handler_type-pack" \
31 + PATH_TRANSLATED="$PWD/.git/git-$handler_type-pack" \
32 + GIT_HTTP_EXPORT_ALL=TRUE \
33 + REQUEST_METHOD=POST \
34 + "$TEST_DIRECTORY"/t5562/invoke-with-content-length.pl \
35 + "$request_body" git http-backend >act.out 2>act.err
36 +}
37 +
38 +ssize_b100dots() {
39 + # hardcoded ((size_t) SSIZE_MAX) + 1
40 + case "$(build_option sizeof-size_t)" in
41 + 8) echo 9223372036854775808;;
42 + 4) echo 2147483648;;
43 + *) die "Unexpected ssize_t size: $(build_option sizeof-size_t)";;
44 + esac
45 +}
46 +
47 +test_expect_success 'setup' '
48 + export HTTP_CONTENT_ENCODING="identity" &&
49 + git config http.receivepack true &&
50 + test_commit c0 &&
51 + test_commit c1 &&
52 + hash_head=$(git rev-parse HEAD) &&
53 + hash_prev=$(git rev-parse HEAD~1) &&
54 + printf "want %s" "$hash_head" | packetize >fetch_body &&
55 + printf 0000 >>fetch_body &&
56 + printf "have %s" "$hash_prev" | packetize >>fetch_body &&
57 + printf done | packetize >>fetch_body &&
58 + test_copy_bytes 10 <fetch_body >fetch_body.trunc &&
59 + hash_next=$(git commit-tree -p HEAD -m next HEAD^{tree}) &&
60 + printf "%s %s refs/heads/newbranch\\0report-status\\n" "$_z40" "$hash_next" | packetize >push_body &&
61 + printf 0000 >>push_body &&
62 + echo "$hash_next" | git pack-objects --stdout >>push_body &&
63 + test_copy_bytes 10 <push_body >push_body.trunc &&
64 + : >empty_body
65 +'
66 +
67 +test_expect_success GZIP 'setup, compression related' '
68 + gzip -c fetch_body >fetch_body.gz &&
69 + test_copy_bytes 10 <fetch_body.gz >fetch_body.gz.trunc &&
70 + gzip -c push_body >push_body.gz &&
71 + test_copy_bytes 10 <push_body.gz >push_body.gz.trunc
72 +'
73 +
74 +test_expect_success 'fetch plain' '
75 + test_http_env upload fetch_body &&
76 + verify_http_result "200 OK"
77 +'
78 +
79 +test_expect_success 'fetch plain truncated' '
80 + test_http_env upload fetch_body.trunc &&
81 + ! verify_http_result "200 OK"
82 +'
83 +
84 +test_expect_success 'fetch plain empty' '
85 + test_http_env upload empty_body &&
86 + ! verify_http_result "200 OK"
87 +'
88 +
89 +test_expect_success GZIP 'fetch gzipped' '
90 + test_env HTTP_CONTENT_ENCODING="gzip" test_http_env upload fetch_body.gz &&
91 + verify_http_result "200 OK"
92 +'
93 +
94 +test_expect_success GZIP 'fetch gzipped truncated' '
95 + test_env HTTP_CONTENT_ENCODING="gzip" test_http_env upload fetch_body.gz.trunc &&
96 + ! verify_http_result "200 OK"
97 +'
98 +
99 +test_expect_success GZIP 'fetch gzipped empty' '
100 + test_env HTTP_CONTENT_ENCODING="gzip" test_http_env upload empty_body &&
101 + ! verify_http_result "200 OK"
102 +'
103 +
104 +test_expect_success GZIP 'push plain' '
105 + test_when_finished "git branch -D newbranch" &&
106 + test_http_env receive push_body &&
107 + verify_http_result "200 OK" &&
108 + git rev-parse newbranch >act.head &&
109 + echo "$hash_next" >exp.head &&
110 + test_cmp act.head exp.head
111 +'
112 +
113 +test_expect_success 'push plain truncated' '
114 + test_http_env receive push_body.trunc &&
115 + ! verify_http_result "200 OK"
116 +'
117 +
118 +test_expect_success 'push plain empty' '
119 + test_http_env receive empty_body &&
120 + ! verify_http_result "200 OK"
121 +'
122 +
123 +test_expect_success GZIP 'push gzipped' '
124 + test_when_finished "git branch -D newbranch" &&
125 + test_env HTTP_CONTENT_ENCODING="gzip" test_http_env receive push_body.gz &&
126 + verify_http_result "200 OK" &&
127 + git rev-parse newbranch >act.head &&
128 + echo "$hash_next" >exp.head &&
129 + test_cmp act.head exp.head
130 +'
131 +
132 +test_expect_success GZIP 'push gzipped truncated' '
133 + test_env HTTP_CONTENT_ENCODING="gzip" test_http_env receive push_body.gz.trunc &&
134 + ! verify_http_result "200 OK"
135 +'
136 +
137 +test_expect_success GZIP 'push gzipped empty' '
138 + test_env HTTP_CONTENT_ENCODING="gzip" test_http_env receive empty_body &&
139 + ! verify_http_result "200 OK"
140 +'
141 +
142 +test_expect_success 'CONTENT_LENGTH overflow ssite_t' '
143 + NOT_FIT_IN_SSIZE=$(ssize_b100dots) &&
144 + env \
145 + CONTENT_TYPE=application/x-git-upload-pack-request \
146 + QUERY_STRING=/repo.git/git-upload-pack \
147 + PATH_TRANSLATED="$PWD"/.git/git-upload-pack \
148 + GIT_HTTP_EXPORT_ALL=TRUE \
149 + REQUEST_METHOD=POST \
150 + CONTENT_LENGTH="$NOT_FIT_IN_SSIZE" \
151 + git http-backend </dev/zero >/dev/null 2>err &&
152 + grep "fatal:.*CONTENT_LENGTH" err
153 +'
154 +
155 +test_done
t/t5562/invoke-with-content-length.pl new
+37
@@ -0,0 +1,37 @@
1 +#!/usr/bin/perl
2 +use 5.008;
3 +use strict;
4 +use warnings;
5 +
6 +my $body_filename = $ARGV[0];
7 +my @command = @ARGV[1 .. $#ARGV];
8 +
9 +# read data
10 +my $body_size = -s $body_filename;
11 +$ENV{"CONTENT_LENGTH"} = $body_size;
12 +open(my $body_fh, "<", $body_filename) or die "Cannot open $body_filename: $!";
13 +my $body_data;
14 +defined read($body_fh, $body_data, $body_size) or die "Cannot read $body_filename: $!";
15 +close($body_fh);
16 +
17 +my $exited = 0;
18 +$SIG{"CHLD"} = sub {
19 + $exited = 1;
20 +};
21 +
22 +# write data
23 +my $pid = open(my $out, "|-", @command);
24 +{
25 + # disable buffering at $out
26 + my $old_selected = select;
27 + select $out;
28 + $| = 1;
29 + select $old_selected;
30 +}
31 +print $out $body_data or die "Cannot write data: $!";
32 +
33 +sleep 60; # is interrupted by SIGCHLD
34 +if (!$exited) {
35 + close($out);
36 + die "Command did not exit after reading whole body";
37 +}