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