374
|
1 #!/usr/bin/perl
|
|
2
|
|
3 # (C) Sergey Kandaurov
|
|
4 # (C) Nginx, Inc.
|
|
5
|
|
6 # Tests for SPDY protocol version 3.1.
|
|
7
|
|
8 ###############################################################################
|
|
9
|
|
10 use warnings;
|
|
11 use strict;
|
|
12
|
|
13 use Test::More;
|
|
14
|
|
15 use IO::Select;
|
|
16
|
|
17 BEGIN { use FindBin; chdir($FindBin::Bin); }
|
|
18
|
|
19 use lib 'lib';
|
|
20 use Test::Nginx;
|
|
21
|
|
22 ###############################################################################
|
|
23
|
|
24 select STDERR; $| = 1;
|
|
25 select STDOUT; $| = 1;
|
|
26
|
|
27 eval {
|
|
28 require Compress::Raw::Zlib;
|
|
29 Compress::Raw::Zlib->Z_OK;
|
|
30 Compress::Raw::Zlib->Z_SYNC_FLUSH;
|
|
31 Compress::Raw::Zlib->Z_NO_COMPRESSION;
|
|
32 Compress::Raw::Zlib->WANT_GZIP_OR_ZLIB;
|
|
33 };
|
|
34 plan(skip_all => 'Compress::Raw::Zlib not installed') if $@;
|
|
35 plan(skip_all => 'win32') if $^O eq 'MSWin32';
|
|
36
|
|
37 my $t = Test::Nginx->new()->has(qw/http proxy cache limit_conn rewrite spdy/);
|
|
38
|
|
39 plan(skip_all => 'no SPDY/3.1') unless $t->has_version('1.5.10');
|
|
40
|
|
41 $t->plan(72)->write_file_expand('nginx.conf', <<'EOF');
|
|
42
|
|
43 %%TEST_GLOBALS%%
|
|
44
|
|
45 daemon off;
|
|
46
|
|
47 events {
|
|
48 }
|
|
49
|
|
50 http {
|
|
51 %%TEST_GLOBALS_HTTP%%
|
|
52
|
|
53 proxy_cache_path %%TESTDIR%%/cache keys_zone=NAME:10m;
|
|
54 limit_conn_zone $binary_remote_addr zone=conn:1m;
|
|
55
|
|
56 server {
|
|
57 listen 127.0.0.1:8080 spdy;
|
|
58 listen 127.0.0.1:8081;
|
|
59 server_name localhost;
|
|
60
|
|
61 location /s {
|
|
62 add_header X-Header X-Foo;
|
|
63 return 200 'body';
|
|
64 }
|
|
65 location /spdy {
|
|
66 return 200 $spdy;
|
|
67 }
|
|
68 location /prio {
|
|
69 return 200 $spdy_request_priority;
|
|
70 }
|
|
71 location /chunk_size {
|
|
72 spdy_chunk_size 1;
|
|
73 return 200 'body';
|
|
74 }
|
|
75 location /redirect {
|
|
76 error_page 405 /s;
|
|
77 return 405;
|
|
78 }
|
|
79 location /proxy {
|
|
80 add_header X-Body "$request_body";
|
|
81 proxy_pass http://127.0.0.1:8081/;
|
|
82 proxy_cache NAME;
|
|
83 proxy_cache_valid 1m;
|
|
84 }
|
|
85 location /t3.html {
|
|
86 limit_conn conn 1;
|
|
87 }
|
|
88 }
|
|
89 }
|
|
90
|
|
91 EOF
|
|
92
|
|
93 $t->run();
|
|
94
|
|
95 # file size is slightly beyond initial window size: 2**16 + 80 bytes
|
|
96
|
|
97 $t->write_file('t1.html',
|
|
98 join('', map { sprintf "X%04dXXX", $_ } (1 .. 8202)));
|
|
99
|
|
100 $t->write_file('t2.html', 'SEE-THIS');
|
|
101 $t->write_file('t3.html', 'SEE-THIS');
|
|
102
|
|
103 my %cframe = (
|
|
104 2 => \&syn_reply,
|
|
105 3 => \&rst_stream,
|
|
106 4 => \&settings,
|
|
107 6 => \&ping,
|
|
108 7 => \&goaway,
|
|
109 9 => \&window_update
|
|
110 );
|
|
111
|
|
112 ###############################################################################
|
|
113
|
|
114 # PING
|
|
115
|
|
116 my $sess = new_session();
|
|
117 spdy_ping($sess, 0x12345678);
|
|
118 my $frames = spdy_read($sess);
|
|
119
|
|
120 my ($frame) = grep { $_->{type} eq "PING" } @$frames;
|
|
121 ok($frame, 'PING frame');
|
|
122 is($frame->{value}, 0x12345678, 'PING payload');
|
|
123
|
|
124 # GET
|
|
125
|
|
126 $sess = new_session();
|
|
127 my $sid1 = spdy_stream($sess, { path => '/s' });
|
|
128 $frames = spdy_read($sess, all => [{ sid => $sid1, fin => 1 }]);
|
|
129
|
|
130 ($frame) = grep { $_->{type} eq "SYN_REPLY" } @$frames;
|
|
131 ok($frame, 'SYN_REPLAY frame');
|
|
132 is($frame->{sid}, $sid1, 'SYN_REPLAY stream');
|
|
133 is($frame->{headers}->{':status'}, 200, 'SYN_REPLAY status');
|
|
134 is($frame->{headers}->{'x-header'}, 'X-Foo', 'SYN_REPLAY header');
|
|
135
|
|
136 ($frame) = grep { $_->{type} eq "DATA" } @$frames;
|
|
137 ok($frame, 'DATA frame');
|
|
138 is($frame->{length}, length 'body', 'DATA length');
|
|
139 is($frame->{data}, 'body', 'DATA payload');
|
|
140
|
|
141 # GET in new SPDY stream in same session
|
|
142
|
|
143 my $sid2 = spdy_stream($sess, { path => '/s' });
|
|
144 $frames = spdy_read($sess, all => [{ sid => $sid2, fin => 1 }]);
|
|
145
|
|
146 ($frame) = grep { $_->{type} eq "SYN_REPLY" } @$frames;
|
|
147 is($frame->{sid}, $sid2, 'SYN_REPLAY stream 2');
|
|
148 is($frame->{headers}->{':status'}, 200, 'SYN_REPLAY status 2');
|
|
149 is($frame->{headers}->{'x-header'}, 'X-Foo', 'SYN_REPLAY header 2');
|
|
150
|
|
151 ($frame) = grep { $_->{type} eq "DATA" } @$frames;
|
|
152 ok($frame, 'DATA frame 2');
|
|
153 is($frame->{sid}, $sid2, 'SYN_REPLAY stream 2');
|
|
154 is($frame->{length}, length 'body', 'DATA length 2');
|
|
155 is($frame->{data}, 'body', 'DATA payload 2');
|
|
156
|
|
157 # HEAD
|
|
158
|
|
159 $sess = new_session();
|
|
160 $sid1 = spdy_stream($sess, { path => '/s', method => 'HEAD' });
|
|
161 $frames = spdy_read($sess, all => [{ sid => $sid1, fin => 1 }]);
|
|
162
|
|
163 ($frame) = grep { $_->{type} eq "SYN_REPLY" } @$frames;
|
|
164 is($frame->{sid}, $sid1, 'SYN_REPLAY stream HEAD');
|
|
165 is($frame->{headers}->{':status'}, 200, 'SYN_REPLAY status HEAD');
|
|
166 is($frame->{headers}->{'x-header'}, 'X-Foo', 'SYN_REPLAY header HEAD');
|
|
167
|
|
168 ($frame) = grep { $_->{type} eq "DATA" } @$frames;
|
|
169 is($frame, undef, 'HEAD no body');
|
|
170
|
|
171 # request header
|
|
172
|
|
173 $sess = new_session();
|
|
174 $sid1 = spdy_stream($sess, { path => '/t1.html',
|
|
175 headers => { "range" => "bytes=10-19" }
|
|
176 });
|
|
177 $frames = spdy_read($sess, all => [{ sid => $sid1, fin => 1 }]);
|
|
178
|
|
179 ($frame) = grep { $_->{type} eq "SYN_REPLY" } @$frames;
|
|
180 is($frame->{headers}->{':status'}, 206, 'SYN_REPLAY status range');
|
|
181
|
|
182 ($frame) = grep { $_->{type} eq "DATA" } @$frames;
|
|
183 is($frame->{length}, 10, 'DATA length range');
|
|
184 is($frame->{data}, '002XXXX000', 'DATA payload range');
|
|
185
|
|
186 # $spdy
|
|
187
|
|
188 $sess = new_session();
|
|
189 $sid1 = spdy_stream($sess, { path => '/spdy' });
|
|
190 $frames = spdy_read($sess, all => [{ sid => $sid1, fin => 1 }]);
|
|
191
|
|
192 ($frame) = grep { $_->{type} eq "DATA" } @$frames;
|
|
193 is($frame->{data}, '3.1', 'spdy variable');
|
|
194
|
|
195 # spdy_chunk_size=1
|
|
196
|
|
197 $sess = new_session();
|
|
198 $sid1 = spdy_stream($sess, { path => '/chunk_size' });
|
|
199 $frames = spdy_read($sess, all => [{ sid => $sid1, fin => 1 }]);
|
|
200
|
|
201 my @data = grep { $_->{type} eq "DATA" } @$frames;
|
|
202 is(@data, 4, 'chunk_size body chunks');
|
|
203 is($data[0]->{data}, 'b', 'chunk_size body 1');
|
|
204 is($data[1]->{data}, 'o', 'chunk_size body 2');
|
|
205 is($data[2]->{data}, 'd', 'chunk_size body 3');
|
|
206 is($data[3]->{data}, 'y', 'chunk_size body 4');
|
|
207
|
|
208 # redirect
|
|
209
|
|
210 $sess = new_session();
|
|
211 $sid1 = spdy_stream($sess, { path => '/redirect' });
|
|
212 $frames = spdy_read($sess, all => [{ sid => $sid1, fin => 1 }]);
|
|
213
|
|
214 ($frame) = grep { $_->{type} eq "SYN_REPLY" } @$frames;
|
|
215 is($frame->{headers}->{':status'}, 405, 'SYN_REPLAY status with redirect');
|
|
216
|
|
217 ($frame) = grep { $_->{type} eq "DATA" } @$frames;
|
|
218 ok($frame, 'DATA frame with redirect');
|
|
219 is($frame->{data}, 'body', 'DATA payload with redirect');
|
|
220
|
|
221 # ensure that HEAD-like requests, i.e., without response body, do not lead to
|
|
222 # client connection close due to cache filling up with upstream response body
|
|
223
|
|
224 TODO: {
|
|
225 local $TODO = 'premature client connection close';
|
|
226
|
|
227 $sess = new_session();
|
|
228 $sid1 = spdy_stream($sess, { path => '/proxy/t2.html', method => 'HEAD' });
|
|
229 $frames = spdy_read($sess, all => [{ sid => $sid1, fin => 1 }]);
|
|
230
|
|
231 $sid2 = spdy_stream($sess, { path => '/' });
|
|
232 $frames = spdy_read($sess, all => [{ sid => $sid1, fin => 1 }]);
|
|
233 ok(grep ({ $_->{type} eq "SYN_REPLY" } @$frames), 'proxy cache headers only');
|
|
234
|
|
235 }
|
|
236
|
|
237 # simple proxy cache test
|
|
238
|
|
239 $sess = new_session();
|
|
240 $sid1 = spdy_stream($sess, { path => '/proxy/t2.html' });
|
|
241 $frames = spdy_read($sess, all => [{ sid => $sid1, fin => 1 }]);
|
|
242
|
|
243 ($frame) = grep { $_->{type} eq "SYN_REPLY" } @$frames;
|
|
244 is($frame->{headers}->{':status'}, '200 OK', 'proxy cache unconditional');
|
|
245
|
|
246 my $ims = $frame->{headers}->{'etag'};
|
|
247
|
|
248 $sid2 = spdy_stream($sess, { path => '/proxy/t2.html',
|
|
249 headers => { "if-none-match" => $ims }
|
|
250 });
|
|
251 $frames = spdy_read($sess, all => [{ sid => $sid2, fin => 1 }]);
|
|
252
|
|
253 ($frame) = grep { $_->{type} eq "SYN_REPLY" } @$frames;
|
|
254 is($frame->{headers}->{':status'}, 304, 'proxy cache conditional');
|
|
255
|
|
256 # request body (uses proxied response)
|
|
257
|
|
258 $sess = new_session();
|
|
259 $sid1 = spdy_stream($sess, { path => '/proxy/t2.html', body => 'TEST' });
|
|
260 $frames = spdy_read($sess, all => [{ sid => $sid1, fin => 1 }]);
|
|
261
|
|
262 ($frame) = grep { $_->{type} eq "SYN_REPLY" } @$frames;
|
|
263 is($frame->{headers}->{'x-body'}, 'TEST', 'request body');
|
|
264
|
|
265 ($frame) = grep { $_->{type} eq "DATA" } @$frames;
|
|
266 is($frame->{length}, length 'SEE-THIS', 'proxied response length');
|
|
267 is($frame->{data}, 'SEE-THIS', 'proxied response');
|
|
268
|
|
269 # WINDOW_UPDATE (client side)
|
|
270
|
|
271 $sess = new_session();
|
|
272 $sid1 = spdy_stream($sess, { path => '/t1.html' });
|
|
273 $frames = spdy_read($sess, all => [{ sid => $sid1, fin => 0 }]);
|
|
274
|
|
275 @data = grep { $_->{type} eq "DATA" } @$frames;
|
|
276 my $sum = eval join '+', map { $_->{length} } @data;
|
|
277 is($sum, 2**16, 'iws - stream blocked on initial window size');
|
|
278
|
|
279 spdy_ping($sess, 0xf00ff00f);
|
|
280 $frames = spdy_read($sess);
|
|
281
|
|
282 ($frame) = grep { $_->{type} eq "PING" } @$frames;
|
|
283 ok($frame, 'iws - PING not blocked');
|
|
284
|
|
285 spdy_window($sess, 2**16, $sid1);
|
|
286 $frames = spdy_read($sess);
|
|
287 is(@$frames, 0, 'iws - updated stream window');
|
|
288
|
|
289 spdy_window($sess, 2**16);
|
|
290 $frames = spdy_read($sess, all => [{ sid => $sid1, fin => 1 }]);
|
|
291
|
|
292 @data = grep { $_->{type} eq "DATA" } @$frames;
|
|
293 $sum = eval join '+', map { $_->{length} } @data;
|
|
294 is($sum, 80, 'iws - updated connection window');
|
|
295
|
|
296 # SETTINGS (initial window size, client side)
|
|
297
|
|
298 $sess = new_session();
|
|
299 spdy_settings($sess, 7 => 2**17);
|
|
300 spdy_window($sess, 2**17);
|
|
301
|
|
302 $sid1 = spdy_stream($sess, { path => '/t1.html' });
|
|
303 $frames = spdy_read($sess, all => [{ sid => $sid1, fin => 1 }]);
|
|
304
|
|
305 @data = grep { $_->{type} eq "DATA" } @$frames;
|
|
306 $sum = eval join '+', map { $_->{length} } @data;
|
|
307 is($sum, 2**16 + 80, 'increased initial window size');
|
|
308
|
|
309 # probe for negative available space in a flow control window
|
|
310
|
|
311 $sess = new_session();
|
|
312 $sid1 = spdy_stream($sess, { path => '/t1.html' });
|
|
313 spdy_read($sess, all => [{ sid => $sid1, fin => 0 }]);
|
|
314
|
|
315 spdy_window($sess, 1);
|
|
316 spdy_settings($sess, 7 => 42);
|
|
317 spdy_window($sess, 1024, $sid1);
|
|
318
|
|
319 $frames = spdy_read($sess);
|
|
320 is(@$frames, 0, 'negative window - no data');
|
|
321
|
|
322 spdy_window($sess, 2**16 - 42 - 1024, $sid1);
|
|
323 $frames = spdy_read($sess);
|
|
324 is(@$frames, 0, 'zero window - no data');
|
|
325
|
|
326 spdy_window($sess, 1, $sid1);
|
|
327 $frames = spdy_read($sess, all => [{ sid => $sid1, fin => 0 }]);
|
|
328 is(@$frames, 1, 'positive window - data');
|
|
329 is(@$frames[0]->{length}, 1, 'positive window - data length');
|
|
330
|
|
331 # stream multiplexing
|
|
332
|
|
333 $sess = new_session();
|
|
334 $sid1 = spdy_stream($sess, { path => '/t1.html' });
|
|
335 $frames = spdy_read($sess, all => [{ sid => $sid1, fin => 0 }]);
|
|
336
|
|
337 @data = grep { $_->{type} eq "DATA" } @$frames;
|
|
338 $sum = eval join '+', map { $_->{length} } @data;
|
|
339 is($sum, 2**16, 'multiple - stream1 data');
|
|
340
|
|
341 $sid2 = spdy_stream($sess, { path => '/t1.html' });
|
|
342 $frames = spdy_read($sess, all => [{ sid => $sid2, fin => 0 }]);
|
|
343
|
|
344 @data = grep { $_->{type} eq "DATA" } @$frames;
|
|
345 is(@data, 0, 'multiple - stream2 no data');
|
|
346
|
|
347 spdy_window($sess, 2**17, $sid1);
|
|
348 spdy_window($sess, 2**17, $sid2);
|
|
349 spdy_window($sess, 2**17);
|
|
350
|
|
351 $frames = spdy_read($sess, all => [
|
|
352 { sid => $sid1, fin => 1 },
|
|
353 { sid => $sid2, fin => 1 }
|
|
354 ]);
|
|
355
|
|
356 @data = grep { $_->{type} eq "DATA" && $_->{sid} == $sid1 } @$frames;
|
|
357 $sum = eval join '+', map { $_->{length} } @data;
|
|
358 is($sum, 80, 'multiple - stream1 remain data');
|
|
359
|
|
360 @data = grep { $_->{type} eq "DATA" && $_->{sid} == $sid2 } @$frames;
|
|
361 $sum = eval join '+', map { $_->{length} } @data;
|
|
362 is($sum, 2**16 + 80, 'multiple - stream2 full data');
|
|
363
|
|
364 # request priority parsing in $spdy_request_priority
|
|
365
|
|
366 $sess = new_session();
|
|
367 $sid1 = spdy_stream($sess, { path => '/prio', prio => 0 });
|
|
368 $frames = spdy_read($sess, all => [{ sid => $sid1, fin => 1 }]);
|
|
369
|
|
370 ($frame) = grep { $_->{type} eq "DATA" } @$frames;
|
|
371 is($frame->{data}, 0, 'priority 0');
|
|
372
|
|
373 $sid1 = spdy_stream($sess, { path => '/prio', prio => 1 });
|
|
374 $frames = spdy_read($sess, all => [{ sid => $sid1, fin => 1 }]);
|
|
375
|
|
376 ($frame) = grep { $_->{type} eq "DATA" } @$frames;
|
|
377 is($frame->{data}, 1, 'priority 1');
|
|
378
|
|
379 $sid1 = spdy_stream($sess, { path => '/prio', prio => 7 });
|
|
380 $frames = spdy_read($sess, all => [{ sid => $sid1, fin => 1 }]);
|
|
381
|
|
382 ($frame) = grep { $_->{type} eq "DATA" } @$frames;
|
|
383 is($frame->{data}, 7, 'priority 7');
|
|
384
|
|
385 # stream muliplexing + priority
|
|
386
|
|
387 TODO: {
|
|
388 local $TODO = 'reversed priority' unless $t->has_version('1.5.11');
|
|
389
|
|
390 $sess = new_session();
|
|
391 $sid1 = spdy_stream($sess, { path => '/t1.html', prio => 7 });
|
|
392 $sid2 = spdy_stream($sess, { path => '/t2.html', prio => 0 });
|
|
393 spdy_read($sess);
|
|
394
|
|
395 spdy_window($sess, 2**17, $sid1);
|
|
396 spdy_window($sess, 2**17, $sid2);
|
|
397 spdy_window($sess, 2**17);
|
|
398
|
|
399 $frames = spdy_read($sess, all => [
|
|
400 { sid => $sid1, fin => 1 },
|
|
401 { sid => $sid2, fin => 1 }
|
|
402 ]);
|
|
403
|
|
404 @data = grep { $_->{type} eq "DATA" } @$frames;
|
|
405 is(join (' ', map { $_->{sid} } @data), "$sid2 $sid1", 'multiple priority 1');
|
|
406
|
|
407 # and vice versa
|
|
408
|
|
409 $sess = new_session();
|
|
410 $sid1 = spdy_stream($sess, { path => '/t1.html', prio => 0 });
|
|
411 $sid2 = spdy_stream($sess, { path => '/t2.html', prio => 7 });
|
|
412 spdy_read($sess);
|
|
413
|
|
414 spdy_window($sess, 2**17, $sid1);
|
|
415 spdy_window($sess, 2**17, $sid2);
|
|
416 spdy_window($sess, 2**17);
|
|
417
|
|
418 $frames = spdy_read($sess, all => [
|
|
419 { sid => $sid1, fin => 1 },
|
|
420 { sid => $sid2, fin => 1 }
|
|
421 ]);
|
|
422
|
|
423 @data = grep { $_->{type} eq "DATA" } @$frames;
|
|
424 is(join (' ', map { $_->{sid} } @data), "$sid1 $sid2", 'multiple priority 2');
|
|
425
|
|
426 }
|
|
427
|
|
428 # limit_conn
|
|
429
|
|
430 $sess = new_session();
|
|
431 spdy_settings($sess, 7 => 1);
|
|
432 $sid1 = spdy_stream($sess, { path => '/t3.html' });
|
|
433 $sid2 = spdy_stream($sess, { path => '/t3.html' });
|
|
434 $frames = spdy_read($sess, all => [
|
|
435 { sid => $sid1, fin => 0 },
|
|
436 { sid => $sid2, fin => 0 }
|
|
437 ]);
|
|
438
|
|
439 ($frame) = grep { $_->{type} eq "SYN_REPLY" && $_->{sid} == $sid1 } @$frames;
|
|
440 is($frame->{headers}->{':status'}, 200, 'conn_limit 1');
|
|
441
|
|
442 ($frame) = grep { $_->{type} eq "SYN_REPLY" && $_->{sid} == $sid2 } @$frames;
|
|
443 is($frame->{headers}->{':status'}, 503, 'conn_limit 2');
|
|
444
|
|
445 # limit_conn + client's RST_STREAM
|
|
446
|
|
447 $sess = new_session();
|
|
448 spdy_settings($sess, 7 => 1);
|
|
449 $sid1 = spdy_stream($sess, { path => '/t3.html' });
|
|
450 spdy_rst($sess, $sid1, 5);
|
|
451 $sid2 = spdy_stream($sess, { path => '/t3.html' });
|
|
452 $frames = spdy_read($sess, all => [
|
|
453 { sid => $sid1, fin => 0 },
|
|
454 { sid => $sid2, fin => 0 }
|
|
455 ]);
|
|
456
|
|
457 ($frame) = grep { $_->{type} eq "SYN_REPLY" && $_->{sid} == $sid1 } @$frames;
|
|
458 is($frame->{headers}->{':status'}, 200, 'RST_STREAM 1');
|
|
459
|
|
460 ($frame) = grep { $_->{type} eq "SYN_REPLY" && $_->{sid} == $sid2 } @$frames;
|
|
461 is($frame->{headers}->{':status'}, 200, 'RST_STREAM 2');
|
|
462
|
|
463 # GOAWAY on SYN_STREAM with even StreamID
|
|
464
|
|
465 TODO: {
|
|
466 local $TODO = 'not yet';
|
|
467
|
|
468 $sess = new_session();
|
|
469 spdy_stream($sess, { path => '/s' }, 2);
|
|
470 $frames = spdy_read($sess);
|
|
471
|
|
472 ($frame) = grep { $_->{type} eq "GOAWAY" } @$frames;
|
|
473 ok($frame, 'even stream - GOAWAY frame');
|
|
474 is($frame->{code}, 1, 'even stream - error code');
|
|
475 is($frame->{sid}, 0, 'even stream - last used stream');
|
|
476
|
|
477 }
|
|
478
|
|
479 # GOAWAY on SYN_STREAM with backward StreamID
|
|
480
|
|
481 TODO: {
|
|
482 local $TODO = 'not yet';
|
|
483
|
|
484 $sess = new_session();
|
|
485 $sid1 = spdy_stream($sess, { path => '/s' }, 3);
|
|
486 spdy_read($sess);
|
|
487
|
|
488 $sid2 = spdy_stream($sess, { path => '/s' }, 1);
|
|
489 $frames = spdy_read($sess);
|
|
490
|
|
491 ($frame) = grep { $_->{type} eq "GOAWAY" } @$frames;
|
|
492 ok($frame, 'backward stream - GOAWAY frame');
|
|
493 is($frame->{code}, 1, 'backward stream - error code');
|
|
494 is($frame->{sid}, $sid1, 'backward stream - last used stream');
|
|
495
|
|
496 }
|
|
497
|
|
498 # RST_STREAM on the second SYN_STREAM with same StreamID
|
|
499
|
|
500 TODO: {
|
|
501 local $TODO = 'not yet';
|
|
502
|
|
503 $sess = new_session();
|
|
504 $sid1 = spdy_stream($sess, { path => '/s' }, 3);
|
|
505 spdy_read($sess, all => [{ sid => $sid1, fin => 1 }]);
|
|
506 $sid2 = spdy_stream($sess, { path => '/s' }, 3);
|
|
507 $frames = spdy_read($sess);
|
|
508
|
|
509 ($frame) = grep { $_->{type} eq "RST_STREAM" } @$frames;
|
|
510 ok($frame, 'dup stream - RST_STREAM frame');
|
|
511 is($frame->{code}, 1, 'dup stream - error code');
|
|
512 is($frame->{sid}, $sid1, 'dup stream - stream');
|
|
513
|
|
514 }
|
|
515
|
|
516 # awkward protocol version
|
|
517
|
|
518 TODO: {
|
|
519 local $TODO = 'not yet' unless $t->has_version('1.5.11');
|
|
520
|
|
521 $sess = new_session();
|
|
522 $sid1 = spdy_stream($sess, { path => '/s', version => 'HTTP/1.10' });
|
|
523 $frames = spdy_read($sess, all => [{ sid => $sid1, fin => 1 }]);
|
|
524
|
|
525 ($frame) = grep { $_->{type} eq "SYN_REPLY" } @$frames;
|
|
526 is($frame->{headers}->{':status'}, 200, 'awkward version');
|
|
527
|
|
528 }
|
|
529
|
|
530 # missing mandatory request header
|
|
531
|
|
532 $sess = new_session();
|
|
533 $sid1 = spdy_stream($sess, { path => '/s', version => '' });
|
|
534 $frames = spdy_read($sess, all => [{ sid => $sid1, fin => 1 }]);
|
|
535
|
|
536 ($frame) = grep { $_->{type} eq "SYN_REPLY" } @$frames;
|
|
537 is($frame->{headers}->{':status'}, 400, 'incomplete headers');
|
|
538
|
|
539 # GOAWAY before closing a connection by server
|
|
540
|
|
541 $t->stop();
|
|
542
|
|
543 TODO: {
|
|
544 local $TODO = 'not yet';
|
|
545
|
|
546 $frames = spdy_read($sess);
|
|
547
|
|
548 ($frame) = grep { $_->{type} eq "GOAWAY" } @$frames;
|
|
549 ok($frame, 'GOAWAY on connection close');
|
|
550
|
|
551 }
|
|
552
|
|
553 ###############################################################################
|
|
554
|
|
555 sub spdy_ping {
|
|
556 my ($sess, $payload) = @_;
|
|
557
|
|
558 raw_write($sess->{socket}, pack("N3", 0x80030006, 0x4, $payload));
|
|
559 }
|
|
560
|
|
561 sub spdy_rst {
|
|
562 my ($sess, $sid, $error) = @_;
|
|
563
|
|
564 raw_write($sess->{socket}, pack("N4", 0x80030003, 0x8, $sid, $error));
|
|
565 }
|
|
566
|
|
567 sub spdy_window {
|
|
568 my ($sess, $win, $stream) = @_;
|
|
569
|
|
570 $stream = 0 unless defined $stream;
|
|
571 raw_write($sess->{socket}, pack("N4", 0x80030009, 8, $stream, $win));
|
|
572 }
|
|
573
|
|
574 sub spdy_settings {
|
|
575 my ($sess, %extra) = @_;
|
|
576
|
|
577 my $cnt = keys %extra;
|
|
578 my $len = 4 + 8 * $cnt;
|
|
579
|
|
580 my $buf = pack "N3", 0x80030004, $len, $cnt;
|
|
581 $buf .= join '', map { pack "N2", $_, $extra{$_} } keys %extra;
|
|
582 raw_write($sess->{socket}, $buf);
|
|
583 }
|
|
584
|
|
585 sub spdy_read {
|
|
586 my ($sess, %extra) = @_;
|
|
587 my ($skip, $length, @got);
|
|
588
|
|
589 again:
|
|
590 my $buf = raw_read($sess->{socket}) or return undef;
|
|
591
|
|
592 for ($skip = 0; $skip < length $buf; $skip += $length + 8) {
|
|
593 my $type = unpack("\@$skip B", $buf);
|
|
594 $length = hex unpack("\@$skip x5 H6", $buf);
|
|
595 if ($type == 0) {
|
|
596 push @got, dframe($skip, $buf);
|
|
597 test_fin($got[-1], $extra{all});
|
|
598 next;
|
|
599 }
|
|
600
|
|
601 my $ctype = unpack("\@$skip x2 n", $buf);
|
|
602 push @got, $cframe{$ctype}($sess, $skip, $buf);
|
|
603 test_fin($got[-1], $extra{all});
|
|
604 }
|
|
605 goto again if %extra && @{$extra{all}};
|
|
606 return \@got;
|
|
607 }
|
|
608
|
|
609 sub test_fin {
|
|
610 my ($frame, $all) = @_;
|
|
611
|
|
612 @{$all} = grep {
|
|
613 !($_->{sid} == $frame->{sid} && $_->{fin} == $frame->{fin})
|
|
614 } @{$all} if defined $frame->{fin};
|
|
615 }
|
|
616
|
|
617 sub dframe {
|
|
618 my ($skip, $buf) = @_;
|
|
619 my %frame;
|
|
620
|
|
621 my $stream = unpack "\@$skip B32", $buf; $skip += 4;
|
|
622 substr($stream, 0, 1) = 0;
|
|
623 $stream = unpack("N", pack("B32", $stream));
|
|
624 $frame{sid} = $stream;
|
|
625
|
|
626 my $flags = unpack "\@$skip B8", $buf; $skip += 1;
|
|
627 $frame{fin} = substr($flags, 7, 1);
|
|
628
|
|
629 my $length = hex (unpack "\@$skip H6", $buf); $skip += 3;
|
|
630 $frame{length} = $length;
|
|
631
|
|
632 $frame{data} = substr($buf, $skip, $length);
|
|
633 $frame{type} = "DATA";
|
|
634 return \%frame;
|
|
635 }
|
|
636
|
|
637 sub spdy_stream {
|
|
638 my ($ctx, $uri, $stream) = @_;
|
|
639 my ($input, $output, $buf);
|
|
640 my ($d, $status);
|
|
641
|
|
642 my $host = $uri->{host} || '127.0.0.1:8080';
|
|
643 my $method = $uri->{method} || 'GET';
|
|
644 my $headers = $uri->{headers} || {};
|
|
645 my $body = $uri->{body};
|
|
646 my $prio = defined $uri->{prio} ? $uri->{prio} : 4;
|
|
647 my $version = defined $uri->{version} ? $uri->{version} : "HTTP/1.1";
|
|
648
|
|
649 if ($stream) {
|
|
650 $ctx->{last_stream} = $stream;
|
|
651 } else {
|
|
652 $ctx->{last_stream} += 2;
|
|
653 }
|
|
654
|
|
655 $buf = pack("NC", 0x80030001, not $body);
|
|
656 $buf .= pack("xxx"); # Length stub
|
|
657 $buf .= pack("N", $ctx->{last_stream}); # Stream-ID
|
|
658 $buf .= pack("N", 0); # Assoc. Stream-ID
|
|
659 $buf .= pack("n", $prio << 13);
|
|
660
|
|
661 my $ent = 4 + keys %{$headers};
|
|
662 $ent++ if $body;
|
|
663 $ent++ if $version;
|
|
664
|
|
665 $input = pack("N", $ent);
|
|
666 $input .= hpack(":host", $host);
|
|
667 $input .= hpack(":method", $method);
|
|
668 $input .= hpack(":path", $uri->{path});
|
|
669 $input .= hpack(":scheme", "http");
|
|
670 if ($version) {
|
|
671 $input .= hpack(":version", $version);
|
|
672 }
|
|
673 if ($body) {
|
|
674 $input .= hpack("content-length", length $body);
|
|
675 }
|
|
676 $input .= join '', map { hpack($_, $headers->{$_}) } keys %{$headers};
|
|
677
|
|
678 $d = $ctx->{zlib}->{d};
|
|
679 $status = $d->deflate($input => \my $start);
|
|
680 $status == Compress::Raw::Zlib->Z_OK or fail "deflate failed";
|
|
681 $status = $d->flush(\my $tail => Compress::Raw::Zlib->Z_SYNC_FLUSH);
|
|
682 $status == Compress::Raw::Zlib->Z_OK or fail "flush failed";
|
|
683 $output = $start . $tail;
|
|
684
|
|
685 my $len = '';
|
|
686 vec($len, 7, 8) = (length $output) + 10;
|
|
687 $buf |= $len;
|
|
688 $buf .= $output;
|
|
689
|
|
690 if (defined $body) {
|
|
691 $buf .= pack "NCxn", $ctx->{last_stream}, 0x01, length $body;
|
|
692 $buf .= $body;
|
|
693 }
|
|
694
|
|
695 raw_write($ctx->{socket}, $buf);
|
|
696 return $ctx->{last_stream};
|
|
697 }
|
|
698
|
|
699 sub syn_reply {
|
|
700 my ($ctx, $skip, $buf) = @_;
|
|
701 my ($i, $status);
|
|
702 my %payload;
|
|
703
|
|
704 $skip += 4;
|
|
705 my $flags = unpack "\@$skip B8", $buf; $skip += 1;
|
|
706 $payload{fin} = substr($flags, 7, 1);
|
|
707
|
|
708 my $length = hex unpack "\@$skip H6", $buf; $skip += 3;
|
|
709 $payload{length} = $length;
|
|
710 $payload{type} = 'SYN_REPLY';
|
|
711
|
|
712 my $stream = unpack "\@$skip B32", $buf; $skip += 4;
|
|
713 substr($stream, 0, 1) = 0;
|
|
714 $stream = unpack("N", pack("B32", $stream));
|
|
715 $payload{sid} = $stream;
|
|
716
|
|
717 my $input = substr($buf, $skip, $length - 4);
|
|
718 $i = $ctx->{zlib}->{i};
|
|
719
|
|
720 $status = $i->inflate($input => \my $out);
|
|
721 fail "Failed: $status" unless $status == Compress::Raw::Zlib->Z_OK;
|
|
722 $payload{headers} = hunpack($out);
|
|
723 return \%payload;
|
|
724 }
|
|
725
|
|
726 sub rst_stream {
|
|
727 my ($ctx, $skip, $buf) = @_;
|
|
728 my %payload;
|
|
729
|
|
730 $skip += 5;
|
|
731 $payload{length} = hex(unpack "\@$skip H6", $buf); $skip += 3;
|
|
732 $payload{type} = 'RST_STREAM';
|
|
733 $payload{sid} = unpack "\@$skip N", $buf; $skip += 4;
|
|
734 $payload{code} = unpack "\@$skip N", $buf;
|
|
735 return \%payload;
|
|
736 }
|
|
737
|
|
738 sub settings {
|
|
739 my ($ctx, $skip, $buf) = @_;
|
|
740 my %payload;
|
|
741
|
|
742 $skip += 4;
|
|
743 $payload{flags} = unpack "\@$skip H", $buf; $skip += 1;
|
|
744 $payload{length} = hex(unpack "\@$skip H6", $buf); $skip += 3;
|
|
745 $payload{type} = 'SETTINGS';
|
|
746
|
|
747 my $nent = unpack "\@$skip N", $buf; $skip += 4;
|
|
748 for (1 .. $nent) {
|
|
749 my $flags = hex unpack "\@$skip H2", $buf; $skip += 1;
|
|
750 my $id = hex unpack "\@$skip H6", $buf; $skip += 3;
|
|
751 $payload{$id}{flags} = $flags;
|
|
752 $payload{$id}{value} = unpack "\@$skip N", $buf; $skip += 4;
|
|
753 }
|
|
754 return \%payload;
|
|
755 }
|
|
756
|
|
757 sub ping {
|
|
758 my ($ctx, $skip, $buf) = @_;
|
|
759 my %payload;
|
|
760
|
|
761 $skip += 5;
|
|
762 $payload{length} = hex(unpack "\@$skip H6", $buf); $skip += 3;
|
|
763 $payload{type} = 'PING';
|
|
764 $payload{value} = unpack "\@$skip N", $buf;
|
|
765 return \%payload;
|
|
766 }
|
|
767
|
|
768 sub goaway {
|
|
769 my ($ctx, $skip, $buf) = @_;
|
|
770 my %payload;
|
|
771
|
|
772 $skip += 5;
|
|
773 $payload{length} = hex unpack "\@$skip H6", $buf; $skip += 3;
|
|
774 $payload{type} = 'GOAWAY';
|
|
775 $payload{sid} = unpack "\@$skip N", $buf; $skip += 4;
|
|
776 $payload{code} = unpack "\@$skip N", $buf;
|
|
777 return \%payload;
|
|
778 }
|
|
779
|
|
780 sub window_update {
|
|
781 my ($ctx, $skip, $buf) = @_;
|
|
782 my %payload;
|
|
783
|
|
784 $skip += 5;
|
|
785
|
|
786 $payload{length} = hex(unpack "\@$skip H6", $buf); $skip += 3;
|
|
787 $payload{type} = 'WINDOW_UPDATE';
|
|
788
|
|
789 my $stream = unpack "\@$skip B32", $buf; $skip += 4;
|
|
790 substr($stream, 0, 1) = 0;
|
|
791 $stream = unpack("N", pack("B32", $stream));
|
|
792 $payload{sid} = $stream;
|
|
793
|
|
794 my $value = unpack "\@$skip B32", $buf;
|
|
795 substr($value, 0, 1) = 0;
|
|
796 $payload{wdelta} = unpack("N", pack("B32", $value));
|
|
797 return \%payload;
|
|
798 }
|
|
799
|
|
800 sub hpack {
|
|
801 my ($name, $value) = @_;
|
|
802
|
|
803 pack("N", length($name)) . $name . pack("N", length($value)) . $value;
|
|
804 }
|
|
805
|
|
806 sub hunpack {
|
|
807 my ($data) = @_;
|
|
808 my %headers;
|
|
809 my $skip = 0;
|
|
810
|
|
811 my $nent = unpack "\@$skip N", $data; $skip += 4;
|
|
812 for (1 .. $nent) {
|
|
813 my $len = unpack("\@$skip N", $data); $skip += 4;
|
|
814 my $name = unpack("\@$skip A$len", $data); $skip += $len;
|
|
815
|
|
816 $len = unpack("\@$skip N", $data); $skip += 4;
|
|
817 my $value = unpack("\@$skip A$len", $data); $skip += $len;
|
|
818
|
|
819 $headers{$name} = $value;
|
|
820 }
|
|
821 return \%headers;
|
|
822 }
|
|
823
|
|
824 sub raw_read {
|
|
825 my ($s) = @_;
|
|
826 my ($got, $buf);
|
|
827
|
|
828 $s->blocking(0);
|
|
829 while (IO::Select->new($s)->can_read(0.4)) {
|
|
830 my $n = $s->sysread($buf, 1024);
|
|
831 last unless $n;
|
|
832 $got .= $buf;
|
|
833 };
|
|
834 log_in($got);
|
|
835 return $got;
|
|
836 }
|
|
837
|
|
838 sub raw_write {
|
|
839 my ($s, $message) = @_;
|
|
840
|
|
841 local $SIG{PIPE} = 'IGNORE';
|
|
842
|
|
843 $s->blocking(0);
|
|
844 while (IO::Select->new($s)->can_write(0.4)) {
|
|
845 log_out($message);
|
|
846 my $n = $s->syswrite($message);
|
|
847 last unless $n;
|
|
848 $message = substr($message, $n);
|
|
849 last unless length $message;
|
|
850 }
|
|
851 }
|
|
852
|
|
853 sub new_session {
|
|
854 my ($d, $i, $status);
|
|
855
|
|
856 ($d, $status) = Compress::Raw::Zlib::Deflate->new(
|
|
857 -WindowBits => 12,
|
|
858 -Dictionary => dictionary(),
|
|
859 -Level => Compress::Raw::Zlib->Z_NO_COMPRESSION
|
|
860 );
|
|
861 fail "Zlib failure: $status" unless $d;
|
|
862
|
|
863 ($i, $status) = Compress::Raw::Zlib::Inflate->new(
|
|
864 -WindowBits => Compress::Raw::Zlib->WANT_GZIP_OR_ZLIB,
|
|
865 -Dictionary => dictionary()
|
|
866 );
|
|
867 fail "Zlib failure: $status" unless $i;
|
|
868
|
|
869 return { zlib => { i => $i, d => $d },
|
|
870 socket => new_socket(), last_stream => -1 };
|
|
871 }
|
|
872
|
|
873 sub new_socket {
|
|
874 my $s;
|
|
875
|
|
876 eval {
|
|
877 local $SIG{ALRM} = sub { die "timeout\n" };
|
|
878 local $SIG{PIPE} = sub { die "sigpipe\n" };
|
|
879 alarm(2);
|
|
880 $s = IO::Socket::INET->new(
|
|
881 Proto => 'tcp',
|
|
882 PeerAddr => '127.0.0.1:8080',
|
|
883 );
|
|
884 alarm(0);
|
|
885 };
|
|
886 alarm(0);
|
|
887
|
|
888 if ($@) {
|
|
889 log_in("died: $@");
|
|
890 return undef;
|
|
891 }
|
|
892
|
|
893 return $s;
|
|
894 }
|
|
895
|
|
896 sub dictionary {
|
|
897 join('', (map pack('N/a*', $_), qw(
|
|
898 options
|
|
899 head
|
|
900 post
|
|
901 put
|
|
902 delete
|
|
903 trace
|
|
904 accept
|
|
905 accept-charset
|
|
906 accept-encoding
|
|
907 accept-language
|
|
908 accept-ranges
|
|
909 age
|
|
910 allow
|
|
911 authorization
|
|
912 cache-control
|
|
913 connection
|
|
914 content-base
|
|
915 content-encoding
|
|
916 content-language
|
|
917 content-length
|
|
918 content-location
|
|
919 content-md5
|
|
920 content-range
|
|
921 content-type
|
|
922 date
|
|
923 etag
|
|
924 expect
|
|
925 expires
|
|
926 from
|
|
927 host
|
|
928 if-match
|
|
929 if-modified-since
|
|
930 if-none-match
|
|
931 if-range
|
|
932 if-unmodified-since
|
|
933 last-modified
|
|
934 location
|
|
935 max-forwards
|
|
936 pragma
|
|
937 proxy-authenticate
|
|
938 proxy-authorization
|
|
939 range
|
|
940 referer
|
|
941 retry-after
|
|
942 server
|
|
943 te
|
|
944 trailer
|
|
945 transfer-encoding
|
|
946 upgrade
|
|
947 user-agent
|
|
948 vary
|
|
949 via
|
|
950 warning
|
|
951 www-authenticate
|
|
952 method
|
|
953 get
|
|
954 status), "200 OK",
|
|
955 qw(version HTTP/1.1 url public set-cookie keep-alive origin)),
|
|
956 "100101201202205206300302303304305306307402405406407408409410",
|
|
957 "411412413414415416417502504505",
|
|
958 "203 Non-Authoritative Information",
|
|
959 "204 No Content",
|
|
960 "301 Moved Permanently",
|
|
961 "400 Bad Request",
|
|
962 "401 Unauthorized",
|
|
963 "403 Forbidden",
|
|
964 "404 Not Found",
|
|
965 "500 Internal Server Error",
|
|
966 "501 Not Implemented",
|
|
967 "503 Service Unavailable",
|
|
968 "Jan Feb Mar Apr May Jun Jul Aug Sept Oct Nov Dec",
|
|
969 " 00:00:00",
|
|
970 " Mon, Tue, Wed, Thu, Fri, Sat, Sun, GMT",
|
|
971 "chunked,text/html,image/png,image/jpg,image/gif,",
|
|
972 "application/xml,application/xhtml+xml,text/plain,",
|
|
973 "text/javascript,public", "privatemax-age=gzip,deflate,",
|
|
974 "sdchcharset=utf-8charset=iso-8859-1,utf-,*,enq=0."
|
|
975 );
|
|
976 }
|
|
977
|
|
978 ###############################################################################
|