1use strict;
2use warnings;
3use Digest::MD5 qw(md5_hex);
4use File::Temp qw(tempfile);
5use Getopt::Long;
6use Net::EmptyPort qw(check_port empty_port);
7use Test::More;
8use URI::Escape;
9use t::Util;
10
11my ($aggregated_mode, $h2o_keepalive, $starlet_keepalive, $starlet_force_chunked, $unix_socket);
12
13GetOptions(
14    "mode=i"                  => sub {
15        (undef, my $m) = @_;
16        $h2o_keepalive = ($m & 1) != 0;
17        $starlet_keepalive = ($m & 2) != 0;
18        $starlet_force_chunked = ($m & 4) != 0;
19        $unix_socket = ($m & 8) != 0;
20    },
21    "h2o-keepalive=i"         => \$h2o_keepalive,
22    "starlet-keepalive=i"     => \$starlet_keepalive,
23    "starlet-force-chunked=i" => \$starlet_force_chunked,
24    "unix-socket=i"           => \$unix_socket,
25) or exit(1);
26
27plan skip_all => 'plackup not found'
28    unless prog_exists('plackup');
29
30plan skip_all => 'Starlet not found'
31    unless system('perl -MStarlet /dev/null > /dev/null 2>&1') == 0;
32plan skip_all => 'skipping unix-socket tests, requires Starlet >= 0.25'
33    if $unix_socket && `perl -MStarlet -e 'print \$Starlet::VERSION'` < 0.25;
34
35my %files = map { do {
36    my $fn = DOC_ROOT . "/$_";
37    +($_ => { size => (stat $fn)[7], md5 => md5_file($fn) });
38} } qw(index.txt halfdome.jpg);
39
40my $huge_file_size = 50 * 1024 * 1024; # should be larger than the mmap_backend threshold of h2o
41my $huge_file = create_data_file($huge_file_size);
42my $huge_file_md5 = md5_file($huge_file);
43
44my ($unix_socket_file, $unix_socket_guard) = do {
45    (undef, my $fn) = tempfile(UNLINK => 0);
46    unlink $fn;
47    +(
48        $fn,
49        make_guard(sub {
50            unlink $fn;
51        }),
52    );
53} if $unix_socket;
54
55my $upstream = $unix_socket_file ? "[unix:$unix_socket_file]" : "127.0.0.1:@{[empty_port()]}";
56
57my $guard = do {
58    local $ENV{FORCE_CHUNKED} = $starlet_force_chunked;
59    my @args = (qw(plackup -s Starlet --max-workers=20 --keepalive-timeout 100 --access-log /dev/null --listen), $unix_socket_file || $upstream);
60    if ($starlet_keepalive) {
61        push @args, "--max-keepalive-reqs=100";
62    }
63    push @args, ASSETS_DIR . "/upstream.psgi";
64    spawn_server(
65        argv     => \@args,
66        is_ready =>  sub {
67            if ($unix_socket_file) {
68                !! -e $unix_socket_file;
69            } else {
70                $upstream =~ /:([0-9]+)$/s
71                    or die "failed to extract port number";
72                check_port($1);
73            }
74        },
75    );
76};
77
78my $server = spawn_h2o(<< "EOT");
79hosts:
80  default:
81    paths:
82      /:
83        proxy.reverse.url: http://$upstream
84      /gzip:
85        proxy.reverse.url: http://$upstream
86        gzip: ON
87      /test-request-uri-noslash:
88        proxy.reverse.url: http://$upstream/echo-request-uri
89        gzip: ON
90      /test-request-uri-withslash:
91        proxy.reverse.url: http://$upstream/echo-request-uri/
92        gzip: ON
93      /files:
94        file.dir: @{[ DOC_ROOT ]}
95reproxy: ON
96@{[ $h2o_keepalive ? "" : "proxy.timeout.keepalive: 0" ]}
97EOT
98
99run_with_curl($server, sub {
100    my ($proto, $port, $curl) = @_;
101    for my $file (sort keys %files) {
102        my $content = `$curl --silent --show-error $proto://127.0.0.1:$port/$file`;
103        is length($content), $files{$file}->{size}, "$proto://127.0.0.1/$file (size)";
104        is md5_hex($content), $files{$file}->{md5}, "$proto://127.0.0.1/$file (md5)";
105    }
106    for my $file (sort keys %files) {
107        my $content = `$curl --silent --show-error --data-binary \@@{[ DOC_ROOT ]}/$file $proto://127.0.0.1:$port/echo`;
108        is length($content), $files{$file}->{size}, "$proto://127.0.0.1/echo (POST, $file, size)";
109        is md5_hex($content), $files{$file}->{md5}, "$proto://127.0.0.1/echo (POST, $file, md5)";
110    }
111    if ($curl !~ /--http2/) {
112        for my $file (sort keys %files) {
113            my $content = `$curl --silent --show-error --header 'Transfer-Encoding: chunked' --data-binary \@@{[ DOC_ROOT ]}/$file $proto://127.0.0.1:$port/echo`;
114            is length($content), $files{$file}->{size}, "$proto://127.0.0.1/echo (POST, chunked, $file, size)";
115            is md5_hex($content), $files{$file}->{md5}, "$proto://127.0.0.1/echo (POST, chunked, $file, md5)";
116        }
117    }
118    my $content = `$curl --silent --show-error --data-binary \@$huge_file $proto://127.0.0.1:$port/echo`;
119    is length($content), $huge_file_size, "$proto://127.0.0.1/echo (POST, mmap-backed, size)";
120    is md5_hex($content), $huge_file_md5, "$proto://127.0.0.1/echo (POST, mmap-backed, md5)";
121    if ($curl !~ /--http2/) {
122        $content = `$curl --silent --show-error --header 'Transfer-Encoding: chunked' --data-binary \@$huge_file $proto://127.0.0.1:$port/echo`;
123        is length($content), $huge_file_size, "$proto://127.0.0.1/echo (POST, chunked, mmap-backed, size)";
124        is md5_hex($content), $huge_file_md5, "$proto://127.0.0.1/echo (POST, chunked, mmap-backed, md5)";
125    }
126    subtest 'rewrite-redirect' => sub {
127        $content = `$curl --silent --dump-header /dev/stdout --max-redirs 0 "$proto://127.0.0.1:$port/?resp:status=302&resp:location=http://@{[uri_escape($upstream)]}/abc"`;
128        like $content, qr{HTTP/[^ ]+ 302\s}m;
129        like $content, qr{^location: ?$proto://127.0.0.1:$port/abc\r$}m;
130    };
131    subtest "x-reproxy-url ($proto)" => sub {
132        my $fetch_test = sub {
133            my $url_prefix = shift;
134            for my $file (sort keys %files) {
135                my $content = `$curl --silent --show-error "$proto://127.0.0.1:$port/404?resp:status=200&resp:x-reproxy-url=$url_prefix$file"`;
136                is length($content), $files{$file}->{size}, "$file (size)";
137                is md5_hex($content), $files{$file}->{md5}, "$file (md5)";
138            }
139        };
140        subtest "abs-url" => sub {
141            $fetch_test->("http://@{[uri_escape($upstream)]}/");
142        };
143        subtest "abs-path" => sub {
144            $fetch_test->("/");
145        };
146        subtest "rel-path" => sub {
147            $fetch_test->("");
148        };
149        my $content = `$curl --silent --show-error "$proto://127.0.0.1:$port/streaming-body?resp:status=200&resp:x-reproxy-url=http://@{[uri_escape($upstream)]}/index.txt"`;
150        is $content, "hello\n", "streaming-body";
151        $content = `$curl --silent "$proto://127.0.0.1:$port/?resp:status=200&resp:x-reproxy-url=https://default/files/index.txt"`;
152        is length($content), $files{"index.txt"}->{size}, "to file handler (size)";
153        is md5_hex($content), $files{"index.txt"}->{md5}, "to file handler (md5)";
154        $content = `$curl --silent "$proto://127.0.0.1:$port/?resp:status=200&resp:x-reproxy-url=http://@{[uri_escape($upstream)]}/?resp:status=302%26resp:location=index.txt"`;
155        is length($content), $files{"index.txt"}->{size}, "reproxy & internal redirect to upstream (size)";
156        is md5_hex($content), $files{"index.txt"}->{md5}, "reproxy & internal redirect to upstream (md5)";
157        $content = `$curl --silent "$proto://127.0.0.1:$port/?resp:status=200&resp:x-reproxy-url=http://@{[uri_escape($upstream)]}/?resp:status=302%26resp:location=https://default/files/index.txt"`;
158        is length($content), $files{"index.txt"}->{size}, "reproxy & internal redirect to file (size)";
159        is md5_hex($content), $files{"index.txt"}->{md5}, "reproxy & internal redirect to file (md5)";
160        $content = `$curl --silent "$proto://127.0.0.1:$port/?resp:status=200&resp:x-reproxy-url=http://default/files"`;
161        is length($content), $files{"index.txt"}->{size}, "redirect handled internally after delegation (size)";
162        is md5_hex($content), $files{"index.txt"}->{md5}, "redirect handled internally after delegation (md5)";
163
164        subtest "keep-alive" => sub {
165            if ($unix_socket) {
166                pass;
167                return;
168            }
169            my ($headers, $body);
170            my $cmd = "$curl --silent --dump-header /dev/stderr \"$proto://127.0.0.1:$port/?resp:status=302&resp:x-reproxy-url=http://@{[uri_escape($upstream)]}/echo-remote-port\"";
171            ($headers, $body) = run_prog($cmd);
172            my $remote_port = $body;
173            ($headers, $body) = run_prog($cmd);
174            if ($h2o_keepalive && $starlet_keepalive) {
175                is $body, $remote_port, "keep-alive is enabled";
176            } else {
177                isnt $body, $remote_port, "keep-alive is disbaled";
178            }
179        };
180    };
181    subtest "x-forwarded ($proto)" => sub {
182        my $resp = `$curl --silent $proto://127.0.0.1:$port/echo-headers`;
183        like $resp, qr/^x-forwarded-for: ?127\.0\.0\.1$/mi, "x-forwarded-for";
184        like $resp, qr/^x-forwarded-proto: ?$proto$/mi, "x-forwarded-proto";
185        like $resp, qr/^via: ?[^ ]+ 127\.0\.0\.1:$port$/mi, "via";
186        $resp = `$curl --silent --header 'X-Forwarded-For: 127.0.0.2' --header 'Via: 2 example.com' $proto://127.0.0.1:$port/echo-headers`;
187        like $resp, qr/^x-forwarded-for: ?127\.0\.0\.2, 127\.0\.0\.1$/mi, "x-forwarded-for (append)";
188        like $resp, qr/^via: ?2 example.com, [^ ]+ 127\.0\.0\.1:$port$/mi, "via (append)";
189    };
190    subtest 'issues/266' => sub {
191        my $resp = `$curl --dump-header /dev/stderr --silent -H 'cookie: a=@{['x' x 4000]}' $proto://127.0.0.1:$port/index.txt 2>&1 > /dev/null`;
192        like $resp, qr{^HTTP/[^ ]+ 200\s}m;
193    };
194    subtest 'gzip' => sub {
195        plan skip_all => 'curl issue #661'
196            if $curl =~ /--http2/;
197        my $resp = `$curl --silent -H Accept-Encoding:gzip $proto://127.0.0.1:$port/gzip/alice.txt | gzip -cd`;
198        is md5_hex($resp), md5_file("@{[DOC_ROOT]}/alice.txt");
199    };
200    subtest 'request uri' => sub {
201        my $resp = `$curl --silent $proto://127.0.0.1:$port/test-request-uri-noslash`;
202        like $resp, qr{^/echo-request-uri$}mi;
203        $resp = `$curl --silent $proto://127.0.0.1:$port/test-request-uri-withslash`;
204        like $resp, qr{^/echo-request-uri/$}mi;
205        $resp = `$curl --silent $proto://127.0.0.1:$port/test-request-uri-noslash?abc=def`;
206        like $resp, qr{^/echo-request-uri\?abc=def$}mi;
207        $resp = `$curl --silent $proto://127.0.0.1:$port/test-request-uri-withslash?abc=def`;
208        like $resp, qr{^/echo-request-uri/\?abc=def$}mi;
209        $resp = `$curl --silent $proto://127.0.0.1:$port/test-request-uri-noslash/abc`;
210        like $resp, qr{^/echo-request-uri/abc$}mi;
211        $resp = `$curl --silent $proto://127.0.0.1:$port/test-request-uri-withslash/abc`;
212        like $resp, qr{^/echo-request-uri/abc$}mi;
213    };
214});
215
216subtest 'HTTP/1 request body streaming and pipelining' => sub {
217    plan skip_all => 'h2load not found'
218        unless prog_exists('h2load');
219    my $doit = sub {
220        my ($nr, $opts, $port) = @_;
221        my $out = `h2load --h1 -m $nr -n $nr -c 1 -t 1 $opts http://127.0.0.1:$port/echo`;
222        like $out, qr{$nr succeeded}m;
223    };
224    $doit->('10', '', $server->{port});
225    $doit->('20', '', $server->{port});
226    $doit->('10', '-d t/50reverse-proxy/hello.txt ', $server->{port});
227    $doit->('10', "-d $huge_file ", $server->{port});
228};
229
230subtest 'nghttp' => sub {
231    plan skip_all => 'nghttp not found'
232        unless prog_exists('nghttp');
233    my $doit = sub {
234        my ($proto, $opt, $port) = @_;
235        for my $file (sort keys %files) {
236            my $content = `nghttp $opt $proto://127.0.0.1:$port/$file`;
237            is length($content), $files{$file}->{size}, "$proto://127.0.0.1/$file (size)";
238            is md5_hex($content), $files{$file}->{md5}, "$proto://127.0.0.1/$file (md5)";
239        }
240        my $out = `nghttp $opt -d t/50reverse-proxy/hello.txt $proto://127.0.0.1:$port/echo`;
241        is $out, "hello\n", "$proto://127.0.0.1/echo (POST)";
242        $out = `nghttp $opt -m 10 $proto://127.0.0.1:$port/index.txt`;
243        is $out, "hello\n" x 10, "$proto://127.0.0.1/index.txt x 10 times";
244        $out = `nghttp $opt -d $huge_file $proto://127.0.0.1:$port/echo`;
245        is length($out), $huge_file_size, "$proto://127.0.0.1/echo (mmap-backed, size)";
246        is md5_hex($out), $huge_file_md5, "$proto://127.0.0.1/echo (mmap-backed, md5)";
247        subtest 'cookies' => sub {
248            plan skip_all => 'nghttp issues #161'
249                if $opt eq '-u';
250            $out = `nghttp $opt -H 'cookie: a=b' -H 'cookie: c=d' $proto://127.0.0.1:$port/echo-headers`;
251            like $out, qr{^cookie: a=b; c=d$}m;
252        };
253        subtest "x-reproxy-url ($proto)" => sub {
254            for my $file (sort keys %files) {
255                my $content = `nghttp $opt "$proto://127.0.0.1:$port/404?resp:status=200&resp:x-reproxy-url=http://@{[uri_escape($upstream)]}/$file"`;
256                is length($content), $files{$file}->{size}, "$file (size)";
257                is md5_hex($content), $files{$file}->{md5}, "$file (md5)";
258            }
259            my $content = `nghttp $opt "$proto://127.0.0.1:$port/streaming-body?resp:status=200&resp:x-reproxy-url=http://@{[uri_escape($upstream)]}/index.txt"`;
260            is $content, "hello\n", "streaming-body";
261            $content = `nghttp $opt "$proto://127.0.0.1:$port/?resp:status=200&resp:x-reproxy-url=https://default/files/index.txt"`;
262            is length($content), $files{"index.txt"}->{size}, "to file handler (size)";
263            is md5_hex($content), $files{"index.txt"}->{md5}, "to file handler (md5)";
264            $content = `nghttp $opt "$proto://127.0.0.1:$port/?resp:status=200&resp:x-reproxy-url=http://@{[uri_escape($upstream)]}/?resp:status=302%26resp:location=index.txt"`;
265            is length($content), $files{"index.txt"}->{size}, "reproxy & internal redirect to upstream (size)";
266            is md5_hex($content), $files{"index.txt"}->{md5}, "reproxy & internal redirect to upstream (md5)";
267            $content = `nghttp $opt "$proto://127.0.0.1:$port/?resp:status=200&resp:x-reproxy-url=http://@{[uri_escape($upstream)]}/?resp:status=302%26resp:location=https://default/files/index.txt"`;
268            is length($content), $files{"index.txt"}->{size}, "reproxy & internal redirect to file (size)";
269            is md5_hex($content), $files{"index.txt"}->{md5}, "reproxy & internal redirect to file (md5)";
270            $content = `nghttp -v $opt "$proto://127.0.0.1:$port/?resp:status=200&resp:x-reproxy-url=http://default/files"`;
271            unlike $content, qr/ :status: 3/, "once delegated, redirects of the file handler should be handled internally";
272        };
273        subtest 'issues/185' => sub {
274            my $out = `nghttp $opt -v "$proto://127.0.0.1:$port/?resp:access-control-allow-origin=%2a"`;
275            is $?, 0;
276            like $out, qr/ access-control-allow-origin: \*$/m;
277        };
278        subtest 'issues/192' => sub {
279            my $cookie = '_yohoushi_session=ZU5tK2FhcllVQ1RGaTZmZE9MUXozZnAzdTdmR250ZjRFU1hNSnZ4Y2JxZm9pYzJJSEpISGFKNmtWcW1HcjBySmUxZzIwNngrdlVIOC9jdmg0R3I3TFR4eVYzaHlFSHdEL1M4dnh1SmRCbVl3ZE5FckZEU1NyRmZveWZwTmVjcVV5V1JhNUd5REIvWjAwQ3RiT1ZBNGVMUkhiN0tWR0c1RzZkSVhrVkdoeW1lWXRDeHJPUW52NUwxSytRTEQrWXdoZ1EvVG9kak9aeUxnUFRNTDB4Vis1RDNUYWVHZm12bDgwL1lTa09MTlJYcGpXN2VUWmdHQ2FuMnVEVDd4c3l1TTJPMXF1dGhYcGRHS2U2bW9UaG0yZGIwQ0ZWVzFsY1hNTkY5cVFvWjNqSWNrQ0pOY1gvMys4UmRHdXJLU1A0ZTZQc3pSK1dKcXlpTEF2djJHLzUwbytwSnVpS0xhdFp6NU9kTDhtcmgxamVXMkI0Nm9Nck1rMStLUmV0TEdUeGFSTjlKSzM0STc3NTlSZ05ZVjJhWUNibkdzY1I1NUg4bG96dWZSeGorYzF4M2tzMGhKSkxmeFBTNkpZS09HTFgrREN4SWd4a29kamRxT3FobDRQZ2xMVUE9PS0tQUxSWU5nWmVTVzRoN09sS3pmUVM3dz09--3a411c0cf59845f0b8ccf61f69b8eb87aa1727ac; path=/; HttpOnly';
280            my $cookie_encoded = $cookie;
281            $cookie_encoded =~ s{([^A-Za-z0-9_])}{sprintf "%%%02x", ord $1}eg;
282            $out = `nghttp $opt -v $proto://127.0.0.1:$port/?resp:set-cookie=$cookie_encoded`;
283            is $?, 0;
284            like $out, qr/ set-cookie: $cookie$/m;
285        };
286    };
287    subtest 'http (upgrade)' => sub {
288        $doit->('http', '-u', $server->{port});
289    };
290    subtest 'http (direct)' => sub {
291        $doit->('http', '', $server->{port});
292    };
293    subtest 'https' => sub {
294        plan skip_all => 'OpenSSL does not support protocol negotiation; it is too old'
295            unless openssl_can_negotiate();
296        $doit->('https', '', $server->{tls_port});
297    };
298};
299
300done_testing;
301