comparison spdy.t @ 374:ac804fd1eb56

Tests: basic SPDY tests.
author Sergey Kandaurov <pluknet@nginx.com>
date Thu, 20 Feb 2014 13:20:35 +0400
parents
children ab2d8abea393
comparison
equal deleted inserted replaced
373:1e6e216b06c2 374:ac804fd1eb56
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 ###############################################################################