File Coverage

blib/lib/Protocol/WebSocket/Frame.pm
Criterion Covered Total %
statement 168 172 97.6
branch 84 94 89.3
condition 28 41 68.2
subroutine 25 26 96.1
pod 16 18 88.8
total 321 351 91.4


line stmt bran cond sub pod time code
1             package Protocol::WebSocket::Frame;
2              
3 13     13   3083 use strict;
  13         25  
  13         328  
4 13     13   60 use warnings;
  13         24  
  13         278  
5              
6 13     13   59 use Config;
  13         22  
  13         428  
7 13     13   4636 use Encode ();
  13         86430  
  13         314  
8 13     13   79 use Scalar::Util 'readonly';
  13         24  
  13         732  
9              
10 13     13   75 use constant MAX_RAND_INT => 2**32;
  13         27  
  13         801  
11 13     13   68 use constant MATH_RANDOM_SECURE => eval "require Math::Random::Secure;";
  13         49  
  13         695  
12              
13             our $MAX_PAYLOAD_SIZE = 65536;
14             our $MAX_FRAGMENTS_AMOUNT = 128;
15              
16             our %TYPES = (
17             continuation => 0x00,
18             text => 0x01,
19             binary => 0x02,
20             ping => 0x09,
21             pong => 0x0a,
22             close => 0x08
23             );
24              
25             sub new {
26 74     74 1 25213 my $class = shift;
27 74 50       183 $class = ref $class if ref $class;
28 74         96 my $buffer;
29              
30 74 100       163 if (@_ == 1) {
31 3         7 $buffer = shift @_;
32             }
33             else {
34 71         178 my %args = @_;
35 71         153 $buffer = delete $args{buffer};
36             }
37              
38 74         168 my $self = {@_};
39 74         128 bless $self, $class;
40              
41 74 100       152 $buffer = '' unless defined $buffer;
42              
43 74 100       216 if (Encode::is_utf8($buffer)) {
44 9         43 $self->{buffer} = Encode::encode('UTF-8', $buffer);
45             }
46             else {
47 65         118 $self->{buffer} = $buffer;
48             }
49              
50 74 100 66     1019 if (defined($self->{type}) && defined($TYPES{$self->{type}})) {
51 20         55 $self->opcode($TYPES{$self->{type}});
52             }
53              
54 74   100     293 $self->{version} ||= 'draft-ietf-hybi-17';
55              
56 74         130 $self->{fragments} = [];
57              
58 74 100 33     302 $self->{max_fragments_amount} ||= $MAX_FRAGMENTS_AMOUNT unless exists $self->{max_fragments_amount};
59 74 100 33     262 $self->{max_payload_size} ||= $MAX_PAYLOAD_SIZE unless exists $self->{max_payload_size};
60              
61 74         257 return $self;
62             }
63              
64             sub version {
65 279     279 1 357 my $self = shift;
66              
67 279         785 return $self->{version};
68             }
69              
70             sub append {
71 219     219 1 1548 my $self = shift;
72              
73 219 100       343 return unless defined $_[0];
74              
75 217         574 $self->{buffer} .= $_[0];
76 217 100       453 $_[0] = '' unless readonly $_[0];
77              
78 217         305 return $self;
79             }
80              
81             sub next {
82 27     27 1 4259 my $self = shift;
83              
84 27         66 my $bytes = $self->next_bytes;
85 27 100       107 return unless defined $bytes;
86              
87 13         45 return Encode::decode('UTF-8', $bytes);
88             }
89              
90             sub fin {
91             @_ > 1 ? $_[0]->{fin} =
92             $_[1]
93             : defined($_[0]->{fin}) ? $_[0]->{fin}
94 474 100   474 1 1152 : 1;
    100          
95             }
96 199 50   199 0 360 sub rsv { @_ > 1 ? $_[0]->{rsv} = $_[1] : $_[0]->{rsv} }
97              
98             sub opcode {
99             @_ > 1 ? $_[0]->{opcode} =
100             $_[1]
101             : defined($_[0]->{opcode}) ? $_[0]->{opcode}
102 186 100   186 1 3035 : 1;
    100          
103             }
104 751 100   751 1 1417 sub masked { @_ > 1 ? $_[0]->{masked} = $_[1] : $_[0]->{masked} }
105              
106 7     7 1 1650 sub is_ping { $_[0]->opcode == 9 }
107 7     7 1 1674 sub is_pong { $_[0]->opcode == 10 }
108 7     7 1 2220 sub is_close { $_[0]->opcode == 8 }
109 6     6 1 46 sub is_continuation { $_[0]->opcode == 0 }
110 7     7 1 1712 sub is_text { $_[0]->opcode == 1 }
111 7     7 1 1692 sub is_binary { $_[0]->opcode == 2 }
112              
113             sub next_bytes {
114 54     54 1 97 my $self = shift;
115              
116 54 100 66     99 if ( $self->version eq 'draft-hixie-75'
117             || $self->version eq 'draft-ietf-hybi-00')
118             {
119 20 100       59 if ($self->{buffer} =~ s/^\xff\x00//) {
120 1         5 $self->opcode(8);
121 1         3 return '';
122             }
123              
124 19 100       98 return unless $self->{buffer} =~ s/^[^\x00]*\x00(.*?)\xff//s;
125              
126 10         39 return $1;
127             }
128              
129 34 100       79 return unless length $self->{buffer} >= 2;
130              
131 29         60 while (length $self->{buffer}) {
132 199         315 my $hdr = substr($self->{buffer}, 0, 1);
133              
134 199         590 my @bits = split //, unpack("B*", $hdr);
135              
136 199         465 $self->fin($bits[0]);
137 199         636 $self->rsv([@bits[1 .. 3]]);
138              
139 199         325 my $opcode = unpack('C', $hdr) & 0b00001111;
140              
141 199         249 my $offset = 1; # FIN,RSV[1-3],OPCODE
142              
143 199         309 my $payload_len = unpack 'C', substr($self->{buffer}, 1, 1);
144              
145 199         265 my $masked = ($payload_len & 0b10000000) >> 7;
146 199         346 $self->masked($masked);
147              
148 199         225 $offset += 1; # + MASKED,PAYLOAD_LEN
149              
150 199         224 $payload_len = $payload_len & 0b01111111;
151 199 100       458 if ($payload_len == 126) {
    100          
152 1 50       4 return unless length($self->{buffer}) >= $offset + 2;
153              
154 1         4 $payload_len = unpack 'n', substr($self->{buffer}, $offset, 2);
155              
156 1         2 $offset += 2;
157             }
158             elsif ($payload_len > 126) {
159 5 50       17 return unless length($self->{buffer}) >= $offset + 4;
160              
161 40         93 my $bits = join '', map { unpack 'B*', $_ } split //,
162 5         22 substr($self->{buffer}, $offset, 8);
163              
164             # Most significant bit must be 0.
165             # And here is a crazy way of doing it %)
166 5         36 $bits =~ s{^.}{0};
167              
168             # Can we handle 64bit numbers?
169 5 50 33     233 if ($Config{ivsize} <= 4 || $Config{longsize} < 8 || $] < 5.010) {
      33        
170 0         0 $bits = substr($bits, 32);
171 0         0 $payload_len = unpack 'N', pack 'B*', $bits;
172             }
173             else {
174 5         29 $payload_len = unpack 'Q>', pack 'B*', $bits;
175             }
176              
177 5         15 $offset += 8;
178             }
179              
180 199 100 100     523 if ($self->{max_payload_size} && $payload_len > $self->{max_payload_size}) {
181 2         6 $self->{buffer} = '';
182 2         24 die "Payload is too big. "
183             . "Deny big message ($payload_len) "
184             . "or increase max_payload_size ($self->{max_payload_size})";
185             }
186              
187 197         236 my $mask;
188 197 100       265 if ($self->masked) {
189 6 50       13 return unless length($self->{buffer}) >= $offset + 4;
190              
191 6         12 $mask = substr($self->{buffer}, $offset, 4);
192 6         8 $offset += 4;
193             }
194              
195 197 50       344 return if length($self->{buffer}) < $offset + $payload_len;
196              
197 197         381 my $payload = substr($self->{buffer}, $offset, $payload_len);
198              
199 197 100       335 if ($self->masked) {
200 6         11 $payload = $self->_mask($payload, $mask);
201             }
202              
203 197         290 substr($self->{buffer}, 0, $offset + $payload_len, '');
204              
205             # Injected control frame
206 197 100 100     235 if (@{$self->{fragments}} && $opcode & 0b1000) {
  197         520  
207 1         4 $self->opcode($opcode);
208 1         5 return $payload;
209             }
210              
211 196 100       286 if ($self->fin) {
212 21 100       27 if (@{$self->{fragments}}) {
  21         43  
213 4         6 $self->opcode(shift @{$self->{fragments}});
  4         8  
214             }
215             else {
216 17         35 $self->opcode($opcode);
217             }
218 21         27 $payload = join '', @{$self->{fragments}}, $payload;
  21         213  
219 21         45 $self->{fragments} = [];
220 21         94 return $payload;
221             }
222             else {
223              
224             # Remember first fragment opcode
225 175 100       202 if (!@{$self->{fragments}}) {
  175         286  
226 6         10 push @{$self->{fragments}}, $opcode;
  6         13  
227             }
228              
229 175         230 push @{$self->{fragments}}, $payload;
  175         294  
230              
231             die "Too many fragments"
232 175 100       214 if @{$self->{fragments}} > $self->{max_fragments_amount};
  175         521  
233             }
234             }
235              
236 3         10 return;
237             }
238              
239             sub to_bytes {
240 85     85 1 802 my $self = shift;
241              
242 85 100 66     142 if ( $self->version eq 'draft-hixie-75'
243             || $self->version eq 'draft-ietf-hybi-00')
244             {
245 4 100 100     15 if ($self->{type} && $self->{type} eq 'close') {
246 1         4 return "\xff\x00";
247             }
248              
249 3         16 return "\x00" . $self->{buffer} . "\xff";
250             }
251              
252 81 100 100     272 if ($self->{max_payload_size} && length $self->{buffer} > $self->{max_payload_size}) {
253 2         16 die "Payload is too big. "
254             . "Send shorter messages or increase max_payload_size";
255             }
256              
257              
258 79         223 my $rsv_set = 0;
259 79 100 66     173 if ( $self->{rsv} && ref( $self->{rsv} ) eq 'ARRAY' ) {
260 7         10 for my $i ( 0 .. @{ $self->{rsv} } - 1 ) {
  7         22  
261 21         40 $rsv_set += $self->{rsv}->[$i] * ( 1 << ( 6 - $i ) );
262             }
263             }
264              
265 79         118 my $string = '';
266 79         128 my $opcode = $self->opcode;
267 79 100       185 $string .= pack 'C', ($opcode | $rsv_set | ($self->fin ? 128 : 0));
268              
269 79         121 my $payload_len = length($self->{buffer});
270 79 100       156 if ($payload_len <= 125) {
    100          
271 72 100       116 $payload_len |= 0b10000000 if $self->masked;
272 72         136 $string .= pack 'C', $payload_len;
273             }
274             elsif ($payload_len <= 0xffff) {
275 1 50       3 $string .= pack 'C', 126 + ($self->masked ? 128 : 0);
276 1         3 $string .= pack 'n', $payload_len;
277             }
278             else {
279 6 50       14 $string .= pack 'C', 127 + ($self->masked ? 128 : 0);
280              
281             # Shifting by an amount >= to the system wordsize is undefined
282 6 50       137 $string .= pack 'N', $Config{ivsize} <= 4 ? 0 : $payload_len >> 32;
283 6         27 $string .= pack 'N', ($payload_len & 0xffffffff);
284             }
285              
286 79 100       131 if ($self->masked) {
287              
288             my $mask = $self->{mask}
289             || (
290 7   66     84 MATH_RANDOM_SECURE
291             ? Math::Random::Secure::irand(MAX_RAND_INT)
292             : int(rand(MAX_RAND_INT))
293             );
294              
295 7         19 $mask = pack 'N', $mask;
296              
297 7         11 $string .= $mask;
298 7         20 $string .= $self->_mask($self->{buffer}, $mask);
299             }
300             else {
301 72         307 $string .= $self->{buffer};
302             }
303              
304 79         295 return $string;
305             }
306              
307             sub to_string {
308 0     0 0 0 my $self = shift;
309              
310 0         0 die 'DO NOT USE';
311             }
312              
313             sub _mask {
314 13     13   17 my $self = shift;
315 13         25 my ($payload, $mask) = @_;
316              
317 13         38 $mask = $mask x (int(length($payload) / 4) + 1);
318 13         21 $mask = substr($mask, 0, length($payload));
319 13         27 $payload = "$payload" ^ $mask;
320              
321 13         25 return $payload;
322             }
323              
324             sub max_payload_size {
325 8     8 1 15 my $self = shift;
326              
327 8         40 return $self->{max_payload_size};
328             }
329              
330             1;
331             __END__