File Coverage

blib/lib/Net/WebSocket/Endpoint.pm
Criterion Covered Total %
statement 105 113 92.9
branch 18 26 69.2
condition 1 2 50.0
subroutine 28 29 96.5
pod 0 12 0.0
total 152 182 83.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 10     10   5239 use strict;
  10         21  
  10         302  
16 10     10   48 use warnings;
  10         20  
  10         247  
17              
18 10     10   683 use Module::Runtime ();
  10         1978  
  10         172  
19              
20 10     10   4646 use Net::WebSocket::Defragmenter ();
  10         28  
  10         214  
21 10     10   3810 use Net::WebSocket::Frame::close ();
  10         30  
  10         223  
22 10     10   4467 use Net::WebSocket::Frame::ping ();
  10         29  
  10         208  
23 10     10   4122 use Net::WebSocket::Frame::pong ();
  10         26  
  10         202  
24 10     10   4234 use Net::WebSocket::Message ();
  10         28  
  10         202  
25 10     10   4374 use Net::WebSocket::PingStore ();
  10         26  
  10         191  
26 10     10   65 use Net::WebSocket::X ();
  10         90  
  10         192  
27              
28 10     10   45 use constant DEFAULT_MAX_PINGS => 3;
  10         37  
  10         14560  
29              
30             sub new {
31 9     9 0 1122 my ($class, %opts) = @_;
32              
33 9         34 my @missing = grep { !length $opts{$_} } qw( out parser );
  18         167  
34              
35 9 50       43 die "Missing: [@missing]" if @missing;
36              
37 9 50       93 if ( !(ref $opts{'out'})->can('write') ) {
38 0         0 die "“out” ($opts{'out'}) needs a write() method!";
39             }
40              
41 9         25 my $self;
42              
43             my $defragger = Net::WebSocket::Defragmenter->new(
44             parser => $opts{'parser'},
45              
46             on_data_frame => $opts{'on_data_frame'},
47              
48             on_control_frame => sub {
49 6     6   53 $self->_handle_control_frame(shift);
50             },
51              
52             on_protocol_error => sub {
53 2     2   8 ( undef, my $msg ) = @_;
54              
55             #For now … there may be some multiplexing extension
56             #that allows some other behavior down the line,
57             #but let’s enforce standard protocol for now.
58 2         26 my $err_frame = Net::WebSocket::Frame::close->new(
59             code => 'PROTOCOL_ERROR',
60             reason => $msg,
61             $self->FRAME_MASK_ARGS(),
62             );
63              
64 2         22 $self->_write_frame($err_frame);
65             },
66 9         160 );
67              
68             $self = {
69             _max_pings => $class->DEFAULT_MAX_PINGS(),
70              
71             _ping_store => Net::WebSocket::PingStore->new(),
72              
73             _defragger => $defragger,
74              
75 9 100       131 (map { defined($opts{$_}) ? ( "_$_" => $opts{$_} ) : () } qw(
  27         135  
76             max_pings
77              
78             on_data_frame
79              
80             out
81             )),
82             };
83              
84 9         44 return bless $self, $class;
85             }
86              
87             sub get_next_message {
88 22     22 0 3524 my ($self) = @_;
89              
90 22         139 $self->_verify_not_closed();
91              
92 22         78 return $self->{'_defragger'}->get_next_message();
93             }
94              
95             sub create_message {
96 2     2 0 959 my ($self, $frame_type, $payload) = @_;
97              
98 2         490 require Net::WebSocket::FrameTypeName;
99              
100 2         8 my $frame_class = Net::WebSocket::FrameTypeName::get_module($frame_type);
101 2 50       27 Module::Runtime::require_module($frame_class) if !$frame_class->can('new');
102              
103 2         37 return Net::WebSocket::Message->new(
104             $frame_class->new(
105             payload => $payload,
106             $self->FRAME_MASK_ARGS(),
107             ),
108             );
109             }
110              
111             sub check_heartbeat {
112 10     10 0 1228 my ($self) = @_;
113              
114 10         42 my $ping_counter = $self->{'_ping_store'}->get_count();
115              
116 10 100       26 if ($ping_counter == $self->{'_max_pings'}) {
117 1         10 $self->close(
118             code => 'POLICY_VIOLATION',
119             reason => "Unanswered ping(s): $ping_counter",
120             );
121             }
122             else {
123 9         25 my $ping_message = $self->{'_ping_store'}->add();
124              
125 9         68 my $ping = Net::WebSocket::Frame::ping->new(
126             payload => $ping_message,
127             $self->FRAME_MASK_ARGS(),
128             );
129              
130 9         38 $self->_write_frame($ping);
131             }
132              
133 10         3202 return;
134             }
135              
136             sub close {
137 1     1 0 5 my ($self, %opts) = @_;
138              
139             my $close = Net::WebSocket::Frame::close->new(
140             $self->FRAME_MASK_ARGS(),
141             code => $opts{'code'} || 'ENDPOINT_UNAVAILABLE',
142 1   50     12 reason => $opts{'reason'},
143             );
144              
145 1         6 return $self->_close_with_frame($close);
146             }
147              
148             sub _close_with_frame {
149 3     3   21 my ($self, $close_frame) = @_;
150              
151 3         34 $self->_write_frame($close_frame);
152              
153 3         1424 $self->{'_sent_close_frame'} = $close_frame;
154              
155 3         11 return $self;
156             }
157              
158             *shutdown = *close;
159              
160             sub is_closed {
161 2     2 0 702 my ($self) = @_;
162 2 100       18 return $self->{'_sent_close_frame'} ? 1 : 0;
163             }
164              
165             sub received_close_frame {
166 1     1 0 302 my ($self) = @_;
167 1         5 return $self->{'_received_close_frame'};
168             }
169              
170             sub sent_close_frame {
171 5     5 0 613 my ($self) = @_;
172 5         25 return $self->{'_sent_close_frame'};
173             }
174              
175             sub die_on_close {
176 0     0 0 0 my ($self) = @_;
177              
178 0         0 $self->{'_no_die_on_close'} = 0;
179              
180 0         0 return;
181             }
182              
183             sub do_not_die_on_close {
184 2     2 0 17 my ($self) = @_;
185              
186 2         13 $self->{'_no_die_on_close'} = 1;
187              
188 2         6 return;
189             }
190              
191             #----------------------------------------------------------------------
192              
193             sub on_ping {
194 3     3 0 7 my ($self, $frame) = @_;
195              
196 3         16 $self->_write_frame(
197             Net::WebSocket::Frame::pong->new(
198             payload => $frame->get_payload(),
199             $self->FRAME_MASK_ARGS(),
200             ),
201             );
202              
203 3         1498 return;
204             }
205              
206             sub on_pong {
207 1     1 0 3 my ($self, $frame) = @_;
208              
209 1         8 $self->{'_ping_store'}->remove( $frame->get_payload() );
210              
211 1         3 return;
212             }
213              
214             #----------------------------------------------------------------------
215              
216             sub _verify_not_closed {
217 22     22   55 my ($self) = @_;
218              
219 22 50       95 die Net::WebSocket::X->create('EndpointAlreadyClosed') if $self->{'_closed'};
220              
221 22         45 return;
222             }
223              
224             sub _handle_control_frame {
225 6     6   20 my ($self, $frame) = @_;
226              
227 6         24 my $type = $frame->get_type();
228              
229 6 100       80 if ($type eq 'close') {
    50          
230 2 50       10 if (!$self->{'_sent_close_frame'}) {
231 2         8 my ($code, $reason) = $frame->get_code_and_reason();
232              
233 2 100       9 if ($code > 4999) {
234 1         3 my $reason_chunk = substr( $reason, 0, 50 );
235 1         6 $reason = "Invalid close code ($code, reason: $reason_chunk) received.";
236 1         2 $code = 'PROTOCOL_ERROR';
237             }
238              
239             $self->_close_with_frame(
240 2         23 Net::WebSocket::Frame::close->new(
241             code => $code,
242             reason => $reason,
243             $self->FRAME_MASK_ARGS(),
244             ),
245             );
246             }
247              
248 2 50       44 if ($self->{'_received_close_frame'}) {
249 0         0 warn sprintf('Extra close frame received! (%v.02x)', $frame->to_bytes());
250             }
251             else {
252 2         39 $self->{'_received_close_frame'} = $frame;
253             }
254              
255 2 50       12 if (!$self->{'_no_die_on_close'}) {
256 0         0 die Net::WebSocket::X->create('ReceivedClose', $frame);
257             }
258             }
259             elsif ( my $handler_cr = $self->can("on_$type") ) {
260 4         21 $handler_cr->( $self, $frame );
261             }
262             else {
263 0         0 my $ref = ref $self;
264 0         0 die Net::WebSocket::X->create(
265             'ReceivedBadControlFrame',
266             "“$ref” cannot handle a control frame of type “$type”",
267             );
268             }
269              
270 6         24 return;
271             }
272              
273             sub _write_frame {
274 17     17   50 my ($self, $frame) = @_;
275              
276 17         96 return $self->{'_out'}->write($frame->to_bytes());
277             }
278              
279             1;