File Coverage

blib/lib/Protocol/WebSocket/Request.pm
Criterion Covered Total %
statement 197 201 98.0
branch 106 114 92.9
condition 34 45 75.5
subroutine 26 27 96.3
pod 12 13 92.3
total 375 400 93.7


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