File Coverage

blib/lib/Net/WebSocket/Endpoint.pm
Criterion Covered Total %
statement 105 113 92.9
branch 18 26 69.2
condition 4 5 80.0
subroutine 28 29 96.5
pod 0 12 0.0
total 155 185 83.7


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 11     11   5098 use strict;
  11         27  
  11         330  
16 11     11   55 use warnings;
  11         24  
  11         256  
17              
18 11     11   576 use Module::Runtime ();
  11         1853  
  11         201  
19              
20 11     11   4734 use Net::WebSocket::Defragmenter ();
  11         30  
  11         250  
21 11     11   3659 use Net::WebSocket::Frame::close ();
  11         28  
  11         222  
22 11     11   4653 use Net::WebSocket::Frame::ping ();
  11         32  
  11         228  
23 11     11   4108 use Net::WebSocket::Frame::pong ();
  11         31  
  11         219  
24 11     11   4365 use Net::WebSocket::Message ();
  11         28  
  11         222  
25 11     11   4502 use Net::WebSocket::PingStore ();
  11         28  
  11         217  
26 11     11   67 use Net::WebSocket::X ();
  11         77  
  11         207  
27              
28 11     11   54 use constant DEFAULT_MAX_PINGS => 3;
  11         20  
  11         15700  
29              
30             sub new {
31 10     10 0 1455 my ($class, %opts) = @_;
32              
33 10         51 my @missing = grep { !length $opts{$_} } qw( out parser );
  20         163  
34              
35 10 50       45 die "Missing: [@missing]" if @missing;
36              
37 10 50       116 if ( !(ref $opts{'out'})->can('write') ) {
38 0         0 die "“out” ($opts{'out'}) needs a write() method!";
39             }
40              
41 10         29 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 7     7   56 $self->_handle_control_frame(shift);
50             },
51              
52             on_protocol_error => sub {
53 2     2   6 ( 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         28 my $err_frame = Net::WebSocket::Frame::close->new(
59             code => 'PROTOCOL_ERROR',
60             reason => $msg,
61             $self->FRAME_MASK_ARGS(),
62             );
63              
64 2         14 $self->_write_frame($err_frame);
65             },
66 10         152 );
67              
68             $self = {
69             _max_pings => $class->DEFAULT_MAX_PINGS(),
70              
71             _ping_store => Net::WebSocket::PingStore->new(),
72              
73             _defragger => $defragger,
74              
75 10 100       131 (map { defined($opts{$_}) ? ( "_$_" => $opts{$_} ) : () } qw(
  30         163  
76             max_pings
77              
78             on_data_frame
79              
80             out
81             )),
82             };
83              
84 10         47 return bless $self, $class;
85             }
86              
87             sub get_next_message {
88 23     23 0 4085 my ($self) = @_;
89              
90 23         104 $self->_verify_not_closed();
91              
92 23         91 return $self->{'_defragger'}->get_next_message();
93             }
94              
95             sub create_message {
96 2     2 0 1482 my ($self, $frame_type, $payload) = @_;
97              
98 2         618 require Net::WebSocket::FrameTypeName;
99              
100 2         11 my $frame_class = Net::WebSocket::FrameTypeName::get_module($frame_type);
101 2 50       37 Module::Runtime::require_module($frame_class) if !$frame_class->can('new');
102              
103 2         45 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 1465 my ($self) = @_;
113              
114 10         48 my $ping_counter = $self->{'_ping_store'}->get_count();
115              
116 10 100       34 if ($ping_counter == $self->{'_max_pings'}) {
117 1         12 $self->close(
118             code => 'POLICY_VIOLATION',
119             reason => "Unanswered ping(s): $ping_counter",
120             );
121             }
122             else {
123 9         28 my $ping_message = $self->{'_ping_store'}->add();
124              
125 9         94 my $ping = Net::WebSocket::Frame::ping->new(
126             payload => $ping_message,
127             $self->FRAME_MASK_ARGS(),
128             );
129              
130 9         33 $self->_write_frame($ping);
131             }
132              
133 10         3377 return;
134             }
135              
136             sub close {
137 1     1 0 6 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     18 reason => $opts{'reason'},
143             );
144              
145 1         8 return $self->_close_with_frame($close);
146             }
147              
148             sub _close_with_frame {
149 4     4   14 my ($self, $close_frame) = @_;
150              
151 4         22 $self->_write_frame($close_frame);
152              
153 4         1995 $self->{'_sent_close_frame'} = $close_frame;
154              
155 4         15 return $self;
156             }
157              
158             *shutdown = *close;
159              
160             sub is_closed {
161 2     2 0 767 my ($self) = @_;
162 2 100       22 return $self->{'_sent_close_frame'} ? 1 : 0;
163             }
164              
165             sub received_close_frame {
166 1     1 0 331 my ($self) = @_;
167 1         6 return $self->{'_received_close_frame'};
168             }
169              
170             sub sent_close_frame {
171 5     5 0 621 my ($self) = @_;
172 5         34 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 3     3 0 42 my ($self) = @_;
185              
186 3         49 $self->{'_no_die_on_close'} = 1;
187              
188 3         9 return;
189             }
190              
191             #----------------------------------------------------------------------
192              
193             sub on_ping {
194 3     3 0 6 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         1424 return;
204             }
205              
206             sub on_pong {
207 1     1 0 15 my ($self, $frame) = @_;
208              
209 1         10 $self->{'_ping_store'}->remove( $frame->get_payload() );
210              
211 1         3 return;
212             }
213              
214             #----------------------------------------------------------------------
215              
216             sub _verify_not_closed {
217 23     23   61 my ($self) = @_;
218              
219 23 50       119 die Net::WebSocket::X->create('EndpointAlreadyClosed') if $self->{'_closed'};
220              
221 23         61 return;
222             }
223              
224             sub _handle_control_frame {
225 7     7   25 my ($self, $frame) = @_;
226              
227 7         29 my $type = $frame->get_type();
228              
229 7 100       61 if ($type eq 'close') {
    50          
230 3 50       13 if (!$self->{'_sent_close_frame'}) {
231 3         13 my ($code, $reason) = $frame->get_code_and_reason();
232              
233 3 100 100     54 if ($code && $code > 4999) {
234 1         5 my $reason_chunk = substr( $reason, 0, 50 );
235 1         5 $reason = "Invalid close code ($code, reason: $reason_chunk) received.";
236 1         4 $code = 'PROTOCOL_ERROR';
237             }
238              
239             $self->_close_with_frame(
240 3         40 Net::WebSocket::Frame::close->new(
241             code => $code,
242             reason => $reason,
243             $self->FRAME_MASK_ARGS(),
244             ),
245             );
246             }
247              
248 3 50       68 if ($self->{'_received_close_frame'}) {
249 0         0 warn sprintf('Extra close frame received! (%v.02x)', $frame->to_bytes());
250             }
251             else {
252 3         52 $self->{'_received_close_frame'} = $frame;
253             }
254              
255 3 50       17 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         14 $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 7         28 return;
271             }
272              
273             sub _write_frame {
274 18     18   46 my ($self, $frame) = @_;
275              
276 18         106 return $self->{'_out'}->write($frame->to_bytes());
277             }
278              
279             1;