File Coverage

blib/lib/Net/WebSocket/Endpoint.pm
Criterion Covered Total %
statement 117 126 92.8
branch 31 40 77.5
condition 1 2 50.0
subroutine 26 27 96.3
pod 0 12 0.0
total 175 207 84.5


line stmt bran cond sub pod time code
1             package Net::WebSocket::Endpoint;
2              
3             =encoding utf-8
4              
5             =head1 NAME
6              
7             Net::WebSocket::Endpoint
8              
9             =head1 DESCRIPTION
10              
11             See L.
12              
13             =cut
14              
15 9     9   3247 use strict;
  9         16  
  9         198  
16 9     9   36 use warnings;
  9         13  
  9         192  
17              
18 9     9   2926 use Net::WebSocket::Frame::close ();
  9         17  
  9         136  
19 9     9   3073 use Net::WebSocket::Frame::ping ();
  9         20  
  9         150  
20 9     9   2800 use Net::WebSocket::Frame::pong ();
  9         19  
  9         129  
21 9     9   2972 use Net::WebSocket::Message ();
  9         18  
  9         149  
22 9     9   2901 use Net::WebSocket::PingStore ();
  9         31  
  9         154  
23 9     9   42 use Net::WebSocket::X ();
  9         12  
  9         126  
24              
25 9     9   34 use constant DEFAULT_MAX_PINGS => 3;
  9         12  
  9         10691  
26              
27             sub new {
28 8     8 0 1034 my ($class, %opts) = @_;
29              
30 8         54 my @missing = grep { !length $opts{$_} } qw( out parser );
  16         108  
31              
32 8 50       39 die "Missing: [@missing]" if @missing;
33              
34 8 50       81 if ( !(ref $opts{'out'})->can('write') ) {
35 0         0 die "“out” ($opts{'out'}) needs a write() method!";
36             }
37              
38             my $self = {
39             _fragments => [],
40              
41             _max_pings => $class->DEFAULT_MAX_PINGS(),
42              
43             _ping_store => Net::WebSocket::PingStore->new(),
44              
45 8 100       87 (map { defined($opts{$_}) ? ( "_$_" => $opts{$_} ) : () } qw(
  32         119  
46             parser
47             max_pings
48              
49             on_data_frame
50              
51             out
52             )),
53             };
54              
55 8         46 return bless $self, $class;
56             }
57              
58             sub get_next_message {
59 21     21 0 2539 my ($self) = @_;
60              
61 21         63 $self->_verify_not_closed();
62              
63 21         26 my $_msg_frame;
64              
65 21 100       61 if ( $_msg_frame = $self->{'_parser'}->get_next_frame() ) {
66 20 100       81 if ($_msg_frame->is_control()) {
67 5         26 $self->_handle_control_frame($_msg_frame);
68             }
69             else {
70 15 50       37 if ($self->{'_on_data_frame'}) {
71 0         0 $self->{'_on_data_frame'}->($_msg_frame);
72             }
73              
74             #Failure cases:
75             # - continuation without prior fragment
76             # - non-continuation within fragment
77              
78 15 100       49 if ( $_msg_frame->get_type() eq 'continuation' ) {
    100          
79 6 100       9 if ( !@{ $self->{'_fragments'} } ) {
  6         18  
80 1         5 $self->_got_continuation_during_non_fragment($_msg_frame);
81             }
82             }
83 9         26 elsif ( @{ $self->{'_fragments'} } ) {
84 1         8 $self->_got_non_continuation_during_fragment($_msg_frame);
85             }
86              
87 13 100       61 if ($_msg_frame->get_fin()) {
88             return Net::WebSocket::Message->new(
89 7         12 splice( @{ $self->{'_fragments'} } ),
  7         33  
90             $_msg_frame,
91             );
92             }
93             else {
94 6         13 push @{ $self->{'_fragments'} }, $_msg_frame;
  6         12  
95             }
96             }
97              
98 11         23 $_msg_frame = undef;
99             }
100              
101 12 100       52 return defined($_msg_frame) ? q<> : undef;
102             }
103              
104             sub create_message {
105 2     2 0 722 my ($self, $frame_type, $payload) = @_;
106              
107 2         381 require Net::WebSocket::FrameTypeName;
108 2         9 require Net::WebSocket::Message;
109              
110 2         8 my $frame_class = Net::WebSocket::FrameTypeName::get_module($frame_type);
111 2 50       26 Module::Load::load($frame_class) if !$frame_class->can('new');
112              
113 2         44 return Net::WebSocket::Message->new(
114             $frame_class->new(
115             payload => $payload,
116             $self->FRAME_MASK_ARGS(),
117             ),
118             );
119             }
120              
121             sub check_heartbeat {
122 10     10 0 913 my ($self) = @_;
123              
124 10         35 my $ping_counter = $self->{'_ping_store'}->get_count();
125              
126 10 100       27 if ($ping_counter == $self->{'_max_pings'}) {
127 1         9 $self->close(
128             code => 'POLICY_VIOLATION',
129             reason => "Unanswered ping(s): $ping_counter",
130             );
131             }
132              
133 10         22 my $ping_message = $self->{'_ping_store'}->add();
134              
135 10         64 my $ping = Net::WebSocket::Frame::ping->new(
136             payload => $ping_message,
137             $self->FRAME_MASK_ARGS(),
138             );
139              
140 10         34 $self->_write_frame($ping);
141              
142 10         2127 return;
143             }
144              
145             sub close {
146 1     1 0 4 my ($self, %opts) = @_;
147              
148             my $close = Net::WebSocket::Frame::close->new(
149             $self->FRAME_MASK_ARGS(),
150             code => $opts{'code'} || 'ENDPOINT_UNAVAILABLE',
151 1   50     11 reason => $opts{'reason'},
152             );
153              
154 1         5 return $self->_close_with_frame($close);
155             }
156              
157             sub _close_with_frame {
158 2     2   12 my ($self, $close_frame) = @_;
159              
160 2         9 $self->_write_frame($close_frame);
161              
162 2         84 $self->{'_sent_close_frame'} = $close_frame;
163              
164 2         6 return $self;
165             }
166              
167             *shutdown = *close;
168              
169             sub is_closed {
170 2     2 0 5262 my ($self) = @_;
171 2 100       13 return $self->{'_sent_close_frame'} ? 1 : 0;
172             }
173              
174             sub received_close_frame {
175 1     1 0 241 my ($self) = @_;
176 1         5 return $self->{'_received_close_frame'};
177             }
178              
179             sub sent_close_frame {
180 5     5 0 632 my ($self) = @_;
181 5         20 return $self->{'_sent_close_frame'};
182             }
183              
184             sub die_on_close {
185 0     0 0 0 my ($self) = @_;
186              
187 0         0 $self->{'_no_die_on_close'} = 0;
188              
189 0         0 return;
190             }
191              
192             sub do_not_die_on_close {
193 1     1 0 6 my ($self) = @_;
194              
195 1         6 $self->{'_no_die_on_close'} = 1;
196              
197 1         2 return;
198             }
199              
200             #----------------------------------------------------------------------
201              
202             sub on_ping {
203 3     3 0 7 my ($self, $frame) = @_;
204              
205 3         12 $self->_write_frame(
206             Net::WebSocket::Frame::pong->new(
207             payload => $frame->get_payload(),
208             $self->FRAME_MASK_ARGS(),
209             ),
210             );
211              
212 3         170 return;
213             }
214              
215             sub on_pong {
216 1     1 0 3 my ($self, $frame) = @_;
217              
218 1         4 $self->{'_ping_store'}->remove( $frame->get_payload() );
219              
220 1         2 return;
221             }
222              
223             #----------------------------------------------------------------------
224              
225             sub _got_continuation_during_non_fragment {
226 1     1   2 my ($self, $frame) = @_;
227              
228 1         3 my $msg = sprintf('Received continuation outside of fragment!');
229              
230             #For now … there may be some multiplexing extension
231             #that allows some other behavior down the line,
232             #but let’s enforce standard protocol for now.
233 1         10 my $err_frame = Net::WebSocket::Frame::close->new(
234             code => 'PROTOCOL_ERROR',
235             reason => $msg,
236             $self->FRAME_MASK_ARGS(),
237             );
238              
239 1         5 $self->_write_frame($err_frame);
240              
241 1         56 die Net::WebSocket::X->create( 'ReceivedBadControlFrame', $msg );
242             }
243              
244             sub _got_non_continuation_during_fragment {
245 1     1   3 my ($self, $frame) = @_;
246              
247 1         4 my $msg = sprintf('Received %s; expected continuation!', $frame->get_type());
248              
249             #For now … there may be some multiplexing extension
250             #that allows some other behavior down the line,
251             #but let’s enforce standard protocol for now.
252 1         13 my $err_frame = Net::WebSocket::Frame::close->new(
253             code => 'PROTOCOL_ERROR',
254             reason => $msg,
255             $self->FRAME_MASK_ARGS(),
256             );
257              
258 1         6 $self->_write_frame($err_frame);
259              
260 1         60 die Net::WebSocket::X->create( 'ReceivedBadDataFrame', $msg );
261             }
262              
263             sub _verify_not_closed {
264 21     21   39 my ($self) = @_;
265              
266 21 50       79 die Net::WebSocket::X->create('EndpointAlreadyClosed') if $self->{'_closed'};
267              
268 21         37 return;
269             }
270              
271             sub _handle_control_frame {
272 5     5   12 my ($self, $frame) = @_;
273              
274 5         15 my $type = $frame->get_type();
275              
276 5 100       43 if ($type eq 'close') {
    50          
277 1 50       3 if (!$self->{'_sent_close_frame'}) {
278 1         4 my ($code, $reason) = $frame->get_code_and_reason();
279              
280 1         8 $self->_close_with_frame(
281             Net::WebSocket::Frame::close->new(
282             code => $code,
283             reason => $reason,
284             $self->FRAME_MASK_ARGS(),
285             ),
286             );
287             }
288              
289 1 50       21 if ($self->{'_received_close_frame'}) {
290 0         0 warn sprintf('Extra close frame received! (%v.02x)', $frame->to_bytes());
291             }
292             else {
293 1         5 $self->{'_received_close_frame'} = $frame;
294             }
295              
296 1 50       3 if (!$self->{'_no_die_on_close'}) {
297 0         0 die Net::WebSocket::X->create('ReceivedClose', $frame);
298             }
299             }
300             elsif ( my $handler_cr = $self->can("on_$type") ) {
301 4         12 $handler_cr->( $self, $frame );
302             }
303             else {
304 0         0 my $ref = ref $self;
305 0         0 die Net::WebSocket::X->create(
306             'ReceivedBadControlFrame',
307             "“$ref” cannot handle a control frame of type “$type”",
308             );
309             }
310              
311 5         12 return;
312             }
313              
314             sub _write_frame {
315 17     17   44 my ($self, $frame) = @_;
316              
317 17         77 return $self->{'_out'}->write($frame->to_bytes());
318             }
319              
320             1;