File Coverage

blib/lib/Protocol/WebSocket/Request.pm
Criterion Covered Total %
statement 197 201 98.0
branch 105 114 92.1
condition 36 48 75.0
subroutine 26 27 96.3
pod 12 13 92.3
total 376 403 93.3


line stmt bran cond sub pod time code
1             package Protocol::WebSocket::Request;
2              
3 15     15   3453 use strict;
  15         29  
  15         382  
4 15     15   65 use warnings;
  15         26  
  15         367  
5              
6 15     15   63 use base 'Protocol::WebSocket::Message';
  15         22  
  15         4792  
7              
8             require Carp;
9 15     15   3946 use MIME::Base64 ();
  15         7374  
  15         352  
10              
11 15     15   4588 use Protocol::WebSocket::Cookie::Request;
  15         39  
  15         27685  
12              
13             sub new {
14 68     68 1 4719 my $self = shift->SUPER::new(@_);
15 68         158 my (%params) = @_;
16              
17 68   100     264 $self->{headers} = $params{headers} || [];
18              
19 68         318 return $self;
20             }
21              
22             sub new_from_psgi {
23 9     9 1 1554 my $class = shift;
24 9 100       41 my $env = @_ > 1 ? {@_} : shift;
25              
26 9 100       297 Carp::croak('env is required') unless keys %$env;
27              
28 7         17 my $version = '';
29              
30 7         11 my $cookies;
31              
32             my $fields = {
33             upgrade => $env->{HTTP_UPGRADE},
34             connection => $env->{HTTP_CONNECTION},
35             host => $env->{HTTP_HOST},
36 7         29 };
37              
38 7 100       26 if ($env->{HTTP_WEBSOCKET_PROTOCOL}) {
    100          
39             $fields->{'websocket-protocol'} =
40 2         6 $env->{HTTP_WEBSOCKET_PROTOCOL};
41             }
42             elsif ($env->{HTTP_SEC_WEBSOCKET_PROTOCOL}) {
43             $fields->{'sec-websocket-protocol'} =
44 3         7 $env->{HTTP_SEC_WEBSOCKET_PROTOCOL};
45             }
46              
47 7 100       22 if (exists $env->{HTTP_SEC_WEBSOCKET_VERSION}) {
48             $fields->{'sec-websocket-version'} =
49 4         10 $env->{HTTP_SEC_WEBSOCKET_VERSION};
50 4 100       13 if ($env->{HTTP_SEC_WEBSOCKET_VERSION} eq '13') {
51 3         7 $version = 'draft-ietf-hybi-17';
52             }
53             else {
54 1         3 $version = 'draft-ietf-hybi-10';
55             }
56             }
57              
58 7 100       18 if ($env->{HTTP_SEC_WEBSOCKET_KEY}) {
    100          
59 4         10 $fields->{'sec-websocket-key'} = $env->{HTTP_SEC_WEBSOCKET_KEY};
60             }
61             elsif ($env->{HTTP_SEC_WEBSOCKET_KEY1}) {
62 1         2 $version = 'draft-ietf-hybi-00';
63 1         2 $fields->{'sec-websocket-key1'} = $env->{HTTP_SEC_WEBSOCKET_KEY1};
64 1         2 $fields->{'sec-websocket-key2'} = $env->{HTTP_SEC_WEBSOCKET_KEY2};
65             }
66              
67 7 100 100     37 if ($version eq 'draft-ietf-hybi-10' || $version eq 'draft-ietf-hybi-17') {
68 4         10 $fields->{'sec-websocket-origin'} = $env->{HTTP_SEC_WEBSOCKET_ORIGIN};
69             }
70             else {
71 3         6 $fields->{origin} = $env->{HTTP_ORIGIN};
72             }
73              
74 7 100       18 if ($env->{HTTP_COOKIE}) {
75 3         14 $cookies = Protocol::WebSocket::Cookie->new->parse($env->{HTTP_COOKIE});
76             }
77              
78             my $self = $class->new(
79             version => $version,
80             fields => $fields,
81             cookies => $cookies,
82             resource_name => "$env->{SCRIPT_NAME}$env->{PATH_INFO}"
83 7 100       43 . ($env->{QUERY_STRING} ? "?$env->{QUERY_STRING}" : "")
84             );
85 7         25 $self->state('body');
86              
87 7 100 66     27 if ( $env->{HTTP_X_FORWARDED_PROTO}
88             && $env->{HTTP_X_FORWARDED_PROTO} eq 'https')
89             {
90 1         4 $self->secure(1);
91             }
92              
93 7         37 return $self;
94             }
95              
96             sub cookies {
97 45 100   45 1 131 if(@_ > 1) {
98 32         147 my $cookie = Protocol::WebSocket::Cookie->new;
99 32 100       136 return unless $_[1];
100              
101 9 50       37 if (my $cookies = $cookie->parse($_[1])) {
102 9         35 $_[0]->{cookies} = $cookies;
103             }
104             } else {
105 13         54 return $_[0]->{cookies};
106             }
107             }
108              
109             sub resource_name {
110 101 100 50 101 1 435 @_ > 1 ? $_[0]->{resource_name} = $_[1] : $_[0]->{resource_name} || '/';
111             }
112              
113 64     64 1 136 sub upgrade { shift->field('Upgrade') }
114 34     34 1 93 sub connection { shift->field('Connection') }
115              
116 20     20 1 53 sub number1 { shift->_number('number1', 'key1', @_) }
117 20     20 1 53 sub number2 { shift->_number('number2', 'key2', @_) }
118              
119 45     45 0 111 sub key { shift->_key('key' => @_) }
120 92     92 1 203 sub key1 { shift->_key('key1' => @_) }
121 45     45 1 86 sub key2 { shift->_key('key2' => @_) }
122              
123             sub to_string {
124 20     20 1 62 my $self = shift;
125              
126 20   100     66 my $version = $self->version || 'draft-ietf-hybi-17';
127              
128 20         41 my $string = '';
129              
130 20 50       51 Carp::croak(qq/resource_name is required/)
131             unless defined $self->resource_name;
132 20         49 $string .= "GET " . $self->resource_name . " HTTP/1.1\x0d\x0a";
133              
134 20         40 $string .= "Upgrade: WebSocket\x0d\x0a";
135 20         42 $string .= "Connection: Upgrade\x0d\x0a";
136              
137 20 50       49 Carp::croak(qq/Host is required/) unless defined $self->host;
138 20         56 $string .= "Host: " . $self->host . "\x0d\x0a";
139              
140 20 100       73 if (ref $self->{cookies} eq 'Protocol::WebSocket::Cookie') {
141 8         34 my $cookie_string = $self->{cookies}->to_string;
142 8 50       42 $string .= 'Cookie: ' . $cookie_string . "\x0d\x0a"
143             if $cookie_string;
144             }
145              
146 20 100       84 my $origin = $self->origin ? $self->origin : 'http://' . $self->host;
147 20 100       87 $origin =~ s{^http:}{https:} if $self->secure;
148 20 100       82 $string .= (
149             $version eq 'draft-ietf-hybi-10'
150             ? "Sec-WebSocket-Origin"
151             : "Origin"
152             )
153             . ': '
154             . $origin
155             . "\x0d\x0a";
156              
157 20 100 100     104 if ($version eq 'draft-ietf-hybi-10' || $version eq 'draft-ietf-hybi-17') {
    100          
    50          
158 9         26 my $key = $self->key;
159              
160 9 100       26 if (!$key) {
161 3         7 $key = '';
162 3         70 $key .= chr(int(rand(256))) for 1 .. 16;
163              
164 3         38 $key = MIME::Base64::encode_base64($key);
165 3         20 $key =~ s{\s+}{}g;
166             }
167              
168             $string
169 9 100       41 .= 'Sec-WebSocket-Protocol: ' . $self->subprotocol . "\x0d\x0a"
170             if defined $self->subprotocol;
171              
172 9         34 $string .= 'Sec-WebSocket-Key: ' . $key . "\x0d\x0a";
173 9 100       40 $string
174             .= 'Sec-WebSocket-Version: '
175             . ($version eq 'draft-ietf-hybi-17' ? 13 : 8)
176             . "\x0d\x0a";
177             }
178             elsif ($version eq 'draft-ietf-hybi-00') {
179 7         19 $self->_generate_keys;
180              
181 7 100       22 $string
182             .= 'Sec-WebSocket-Protocol: ' . $self->subprotocol . "\x0d\x0a"
183             if defined $self->subprotocol;
184              
185 7         19 $string .= 'Sec-WebSocket-Key1: ' . $self->key1 . "\x0d\x0a";
186 7         19 $string .= 'Sec-WebSocket-Key2: ' . $self->key2 . "\x0d\x0a";
187              
188 7         18 $string .= 'Content-Length: ' . length($self->challenge) . "\x0d\x0a";
189             }
190             elsif ($version eq 'draft-hixie-75') {
191 4 100       9 $string .= 'WebSocket-Protocol: ' . $self->subprotocol . "\x0d\x0a"
192             if defined $self->subprotocol;
193             }
194             else {
195 0         0 Carp::croak('Version ' . $self->version . ' is not supported');
196             }
197 20         36 my @headers = @{$self->{headers}};
  20         51  
198 20         93 while (my ($key, $value) = splice @headers, 0, 2) {
199 2         7 $key =~ s{[\x0d\x0a]}{}gsm;
200 2         4 $value =~ s{[\x0d\x0a]}{}gsm;
201              
202 2         10 $string .= "$key: $value\x0d\x0a";
203             }
204              
205 20         40 $string .= "\x0d\x0a";
206              
207 20 100       57 $string .= $self->challenge if $version eq 'draft-ietf-hybi-00';
208              
209 20         127 return $string;
210             }
211              
212             sub parse {
213 200     200 1 413 my $self = shift;
214              
215 200         555 my $retval = $self->SUPER::parse($_[0]);
216              
217 200 100 100     745 if (!$self->{finalized} && ($self->is_body || $self->is_done)) {
      100        
218 30         65 $self->{finalized} = 1;
219              
220 30 100 66     72 if ($self->key1 && $self->key2) {
    100          
221 7         20 $self->version('draft-ietf-hybi-00');
222             }
223             elsif ($self->key) {
224 11 100       30 if ($self->field('sec-websocket-version') eq '13') {
225 7         25 $self->version('draft-ietf-hybi-17');
226             }
227             else {
228 4         15 $self->version('draft-ietf-hybi-10');
229             }
230             }
231             else {
232 12         29 $self->version('draft-hixie-75');
233             }
234              
235 30 100       92 if (!$self->_finalize) {
236 2         8 $self->error('Not a valid request');
237 2         8 return;
238             }
239             }
240              
241 198         649 return $retval;
242             }
243              
244             sub _parse_first_line {
245 27     27   61 my ($self, $line) = @_;
246              
247 27         86 my ($req, $resource_name, $http) = split ' ' => $line;
248              
249 27 100 66     161 unless ($req && $resource_name && $http) {
      100        
250 2         9 $self->error('Wrong request line');
251 2         33 return;
252             }
253              
254 25 50 33     100 unless ($req eq 'GET' && $http eq 'HTTP/1.1') {
255 0         0 $self->error('Wrong method or http version');
256 0         0 return;
257             }
258              
259 25         76 $self->resource_name($resource_name);
260              
261 25         71 return $self;
262             }
263              
264             sub _parse_body {
265 32     32   55 my $self = shift;
266              
267 32 100 66     93 if ($self->key1 && $self->key2) {
268 8 100       27 return 1 if length $self->{buffer} < 8;
269              
270 6         18 my $challenge = substr $self->{buffer}, 0, 8, '';
271 6         29 $self->challenge($challenge);
272             }
273              
274 30 100       106 if (length $self->{buffer}) {
275 1         3 $self->error('Leftovers');
276 1         2 return;
277             }
278              
279 29         61 return $self;
280             }
281              
282             sub _number {
283 40     40   61 my $self = shift;
284 40         79 my ($name, $key, $value) = @_;
285              
286 40 100       83 if (defined $value) {
287 2         4 $self->{$name} = $value;
288 2         3 return $self;
289             }
290              
291 38 100       127 return $self->{$name} if defined $self->{$name};
292              
293 18   33     99 return $self->{$name} ||= $self->_extract_number($self->$key);
294             }
295              
296             sub _key {
297 182     182   257 my $self = shift;
298 182         243 my $name = shift;
299 182         248 my $value = shift;
300              
301 182 100       335 unless (defined $value) {
302 170 100       351 if (my $value = delete $self->{$name}) {
303 9         28 $self->field("Sec-WebSocket-" . ucfirst($name) => $value);
304             }
305              
306 170         529 return $self->field("Sec-WebSocket-" . ucfirst($name));
307             }
308              
309 12         69 $self->field("Sec-WebSocket-" . ucfirst($name) => $value);
310              
311 12         22 return $self;
312             }
313              
314             sub _generate_keys {
315 7     7   10 my $self = shift;
316              
317 7 100       16 unless ($self->key1) {
318 1         4 my ($number, $key) = $self->_generate_key;
319 1         4 $self->number1($number);
320 1         3 $self->key1($key);
321             }
322              
323 7 100       16 unless ($self->key2) {
324 1         3 my ($number, $key) = $self->_generate_key;
325 1         3 $self->number2($number);
326 1         2 $self->key2($key);
327             }
328              
329 7 100       17 $self->challenge($self->_generate_challenge) unless $self->challenge;
330              
331 7         15 return $self;
332             }
333              
334             sub _generate_key {
335 2     2   3 my $self = shift;
336              
337             # A random integer from 1 to 12 inclusive
338 2         55 my $spaces = int(rand(12)) + 1;
339              
340             # The largest integer not greater than 4,294,967,295 divided by spaces
341 2         5 my $max = int(4_294_967_295 / $spaces);
342              
343             # A random integer from 0 to $max inclusive
344 2         3 my $number = int(rand($max + 1));
345              
346             # The result of multiplying $number and $spaces together
347 2         4 my $product = $number * $spaces;
348              
349             # A string consisting of $product, expressed in base ten
350 2         5 my $key = "$product";
351              
352             # Insert between one and twelve random characters from the ranges U+0021
353             # to U+002F and U+003A to U+007E into $key at random positions.
354 2         3 my $random_characters = int(rand(12)) + 1;
355              
356 2         5 for (1 .. $random_characters) {
357              
358             # From 0 to the last position
359 12         19 my $random_position = int(rand(length($key) + 1));
360              
361             # Random character
362 12 100       24 my $random_character = chr(
363             int(rand(2))
364             ? int(rand(0x2f - 0x21 + 1)) + 0x21
365             : int(rand(0x7e - 0x3a + 1)) + 0x3a
366             );
367              
368             # Insert random character at random position
369 12         18 substr $key, $random_position, 0, $random_character;
370             }
371              
372             # Insert $spaces U+0020 SPACE characters into $key at random positions
373             # other than the start or end of the string.
374 2         5 for (1 .. $spaces) {
375              
376             # From 1 to the last-1 position
377 10         13 my $random_position = int(rand(length($key) - 1)) + 1;
378              
379             # Insert
380 10         15 substr $key, $random_position, 0, ' ';
381             }
382              
383 2         6 return ($number, $key);
384             }
385              
386             sub _generate_challenge {
387 1     1   3 my $self = shift;
388              
389             # A string consisting of eight random bytes (or equivalently, a random 64
390             # bit integer encoded in big-endian order).
391 1         2 my $challenge = '';
392              
393 1         5 $challenge .= chr(int(rand(256))) for 1 .. 8;
394              
395 1         4 return $challenge;
396             }
397              
398             sub _finalize {
399 30     30   52 my $self = shift;
400              
401 30 50 33     66 return unless $self->upgrade && lc $self->upgrade eq 'websocket';
402              
403 30         103 my $connection = $self->connection;
404 30 50       67 return unless $connection;
405              
406 30         109 my @connections = split /\s*,\s*/, $connection;
407 30 100       69 return unless grep { lc $_ eq 'upgrade' } @connections;
  33         126  
408              
409 29   66     73 my $origin = $self->field('Sec-WebSocket-Origin') || $self->field('Origin');
410             #return unless $origin;
411 29         114 $self->origin($origin);
412              
413 29 50       69 if (defined $self->origin) {
414 29 100       77 $self->secure(1) if $self->origin =~ m{^https:};
415             }
416              
417 29         80 my $host = $self->field('Host');
418 29 100       77 return unless $host;
419 28         119 $self->host($host);
420              
421 28   100     64 my $subprotocol = $self->field('Sec-WebSocket-Protocol')
422             || $self->field('WebSocket-Protocol');
423 28 100       93 $self->subprotocol($subprotocol) if $subprotocol;
424              
425 28         107 $self->cookies($self->field('Cookie'));
426 28         109 return $self;
427             }
428              
429 0     0     sub _build_cookie { Protocol::WebSocket::Cookie::Request->new }
430              
431             1;
432             __END__