File Coverage

blib/lib/Tangence/Server.pm
Criterion Covered Total %
statement 259 264 98.1
branch 23 40 57.5
condition 4 4 100.0
subroutine 40 40 100.0
pod 4 21 19.0
total 330 369 89.4


line stmt bran cond sub pod time code
1             # You may distribute under the terms of either the GNU General Public License
2             # or the Artistic License (the same terms as Perl itself)
3             #
4             # (C) Paul Evans, 2011-2021 -- leonerd@leonerd.org.uk
5              
6             package Tangence::Server 0.30;
7              
8 9     9   28165 use v5.26;
  9         36  
9 9     9   49 use warnings;
  9         20  
  9         263  
10 9     9   47 use experimental 'signatures';
  9         25  
  9         44  
11              
12 9     9   974 use base qw( Tangence::Stream );
  9         23  
  9         3835  
13              
14 9     9   72 use Carp;
  9         20  
  9         584  
15              
16 9     9   52 use Scalar::Util qw( weaken );
  9         36  
  9         525  
17 9     9   59 use Sub::Util 1.40 qw( set_subname );
  9         180  
  9         454  
18 9     9   60 use Feature::Compat::Try;
  9         22  
  9         65  
19              
20 9     9   996 use Tangence::Constants;
  9         29  
  9         1666  
21 9     9   71 use Tangence::Types;
  9         25  
  9         617  
22 9     9   4186 use Tangence::Server::Context;
  9         25  
  9         334  
23              
24 9     9   61 use Struct::Dumb;
  9         20  
  9         110  
25             struct CursorObject => [qw( cursor obj )];
26              
27             # We will accept any version back to 3
28 9     9   848 use constant VERSION_MINOR_MIN => 3;
  9         23  
  9         41777  
29              
30             =head1 NAME
31              
32             C - mixin class for building a C server
33              
34             =head1 SYNOPSIS
35              
36             This class is a mixin, it cannot be directly constructed
37              
38             package Example::Server;
39             use base qw( Base::Server Tangence::Server );
40              
41             sub new
42             {
43             my $class = shift;
44             my %args = @_;
45              
46             my $registry = delete $args{registry};
47              
48             my $self = $class->SUPER::new( %args );
49              
50             $self->registry( $registry );
51              
52             return $self;
53             }
54              
55             sub tangence_write
56             {
57             my $self = shift;
58             $self->write( $_[0] );
59             }
60              
61             sub on_read
62             {
63             my $self = shift;
64             $self->tangence_readfrom( $_[0] );
65             }
66              
67             =head1 DESCRIPTION
68              
69             This module provides mixin to implement a C server connection. It
70             should be mixed in to an object used to represent a single connection from a
71             client. It provides a location for the objects in server to store information
72             about the client connection, and coordinates passing messages between the
73             client and the objects in the server.
74              
75             This is a subclass of L which provides implementations of
76             the required C methods. A class mixing in C
77             must still provide the C method required for sending data to the
78             client.
79              
80             For an example of a class that uses this mixin, see
81             L.
82              
83             =cut
84              
85             =head1 PROVIDED METHODS
86              
87             The following methods are provided by this mixin.
88              
89             =cut
90              
91 9   100 9 0 63 sub subscriptions { shift->{subscriptions} ||= [] }
92 49   100 49 0 345 sub watches { shift->{watches} ||= [] }
93              
94             =head2 registry
95              
96             $server->registry( $registry )
97              
98             $registry = $server->registry
99              
100             Accessor to set or obtain the L object for the server.
101              
102             =cut
103              
104             sub registry
105             {
106 75     75 1 1245 my $self = shift;
107 75 100       231 $self->{registry} = shift if @_;
108 75         307 return $self->{registry};
109             }
110              
111             sub tangence_closed
112             {
113 1     1 1 2159 my $self = shift;
114 1         8 $self->SUPER::tangence_closed;
115              
116 1 50       13 if( my $subscriptions = $self->subscriptions ) {
117 1         4 foreach my $s ( @$subscriptions ) {
118 0         0 my ( $object, $event, $id ) = @$s;
119 0         0 $object->unsubscribe_event( $event, $id );
120             }
121              
122 1         3 undef @$subscriptions;
123             }
124              
125 1 50       4 if( my $watches = $self->watches ) {
126 1         4 foreach my $w ( @$watches ) {
127 4         14 my ( $object, $prop, $id ) = @$w;
128 4         14 $object->unwatch_property( $prop, $id );
129             }
130              
131 1         4 undef @$watches;
132             }
133              
134 1 50       4 if( my $cursors = $self->peer_hascursor ) {
135 1         6 foreach my $cursorobj ( values %$cursors ) {
136 0         0 $self->drop_cursorobj( $cursorobj );
137             }
138             }
139             }
140              
141 54         86 sub get_by_id ( $self, $id )
142 54     54 0 91 {
  54         72  
  54         82  
143             # Only permit the client to interact with objects they've already been
144             # sent, so they cannot gain access by inventing object IDs
145 54 100       146 $self->peer_hasobj->{$id} or
146             die "Access not allowed to object with id $id\n";
147              
148 48 50       126 my $obj = $self->registry->get_by_id( $id ) or
149             die "No such object with id $id\n";
150              
151 48         119 return $obj;
152             }
153              
154 4         10 sub handle_request_CALL ( $self, $token, $message )
  4         163  
155 4     4 0 9 {
  4         9  
  4         6  
156 4         143 my $ctx = Tangence::Server::Context->new( $self, $token );
157              
158 4         8 my $response;
159             try {
160             my $objid = $message->unpack_int();
161              
162             my $object = $self->get_by_id( $objid );
163              
164             $response = $object->handle_request_CALL( $ctx, $message );
165             }
166 4         12 catch ( $e ) {
167             return $ctx->responderr( $e );
168             }
169              
170 3         10 $ctx->respond( $response );
171             }
172              
173 3         5 sub handle_request_SUBSCRIBE ( $self, $token, $message )
  3         8  
174 3     3 0 6 {
  3         6  
  3         5  
175 3         17 my $ctx = Tangence::Server::Context->new( $self, $token );
176              
177 3         16 my $response;
178             try {
179             my $objid = $message->unpack_int();
180             my $event = $message->unpack_str();
181              
182             my $object = $self->get_by_id( $objid );
183              
184             weaken( my $weakself = $self );
185              
186             my $id = $object->subscribe_event( $event,
187             set_subname "__SUBSCRIBE($event)__" => sub {
188 2 50   2   7 $weakself or return;
189 2         4 my $object = shift;
190              
191 2         15 my $message = $object->generate_message_EVENT( $weakself, $event, @_ );
192             $weakself->request(
193             request => $message,
194 2         14 on_response => sub { "IGNORE" },
195 2         38 );
196             }
197             );
198              
199             push @{ $self->subscriptions }, [ $object, $event, $id ];
200              
201             $response = Tangence::Message->new( $self, MSG_SUBSCRIBED )
202             }
203 3         11 catch ( $e ) {
204             return $ctx->responderr( $e );
205             }
206              
207 2         9 $ctx->respond( $response );
208             }
209              
210 2         7 sub handle_request_UNSUBSCRIBE ( $self, $token, $message )
  2         5  
211 2     2 0 17 {
  2         4  
  2         3  
212 2         15 my $ctx = Tangence::Server::Context->new( $self, $token );
213              
214 2         4 my $response;
215             try {
216             my $objid = $message->unpack_int();
217             my $event = $message->unpack_str();
218              
219             my $object = $self->get_by_id( $objid );
220              
221             my $edef = $object->can_event( $event ) or
222             die "Object cannot respond to event $event\n";
223              
224             # Delete from subscriptions and obtain id
225             my $id;
226             @{ $self->subscriptions } = grep { $_->[0] == $object and $_->[1] eq $event and ( $id = $_->[2], 0 ) or 1 }
227             @{ $self->subscriptions };
228             defined $id or
229             die "Not subscribed to $event\n";
230              
231             $object->unsubscribe_event( $event, $id );
232              
233             $response = Tangence::Message->new( $self, MSG_OK )
234             }
235 2         7 catch ( $e ) {
236             return $ctx->responderr( $e );
237             }
238              
239 2         8 $ctx->respond( $response );
240             }
241              
242 5         13 sub handle_request_GETPROP ( $self, $token, $message )
  5         8  
243 5     5 0 11 {
  5         11  
  5         8  
244 5         27 my $ctx = Tangence::Server::Context->new( $self, $token );
245              
246 5         20 my $response;
247             try {
248             my $objid = $message->unpack_int();
249              
250             my $object = $self->get_by_id( $objid );
251              
252             $response = $object->handle_request_GETPROP( $ctx, $message )
253             }
254 5         15 catch ( $e ) {
255             return $ctx->responderr( $e );
256             }
257              
258 4         15 $ctx->respond( $response );
259             }
260              
261 4         6 sub handle_request_GETPROPELEM ( $self, $token, $message )
  4         8  
262 4     4 0 7 {
  4         6  
  4         6  
263 4         19 my $ctx = Tangence::Server::Context->new( $self, $token );
264              
265 4         8 my $response;
266             try {
267             my $objid = $message->unpack_int();
268              
269             my $object = $self->get_by_id( $objid );
270              
271             $response = $object->handle_request_GETPROPELEM( $ctx, $message )
272             }
273 4         10 catch ( $e ) {
274             return $ctx->responderr( $e );
275             }
276              
277 4         12 $ctx->respond( $response );
278             }
279              
280 9         14 sub handle_request_SETPROP ( $self, $token, $message )
  9         15  
281 9     9 0 15 {
  9         14  
  9         11  
282 9         40 my $ctx = Tangence::Server::Context->new( $self, $token );
283              
284 9         19 my $response;
285             try {
286             my $objid = $message->unpack_int();
287              
288             my $object = $self->get_by_id( $objid );
289              
290             $response = $object->handle_request_SETPROP( $ctx, $message )
291             }
292 9         24 catch ( $e ) {
293             return $ctx->responderr( $e );
294             }
295              
296 7         34 $ctx->respond( $response );
297             }
298              
299             *handle_request_WATCH = \&_handle_request_WATCHany;
300             *handle_request_WATCH_CUSR = \&_handle_request_WATCHany;
301 19         33 sub _handle_request_WATCHany ( $self, $token, $message )
  19         35  
302 19     19   35 {
  19         34  
  19         30  
303 19         122 my $ctx = Tangence::Server::Context->new( $self, $token );
304              
305 19         68 my ( $want_initial, $object, $prop );
306              
307 19         0 my $response;
308             try {
309             my $objid = $message->unpack_int();
310             $prop = $message->unpack_str();
311              
312             $object = $self->get_by_id( $objid );
313              
314             my $pdef = $object->can_property( $prop ) or
315             die "Object does not have property $prop\n";
316              
317             $self->_install_watch( $object, $prop );
318              
319             if( $message->code == MSG_WATCH ) {
320             $want_initial = $message->unpack_bool();
321              
322             $response = Tangence::Message->new( $self, MSG_WATCHING )
323             }
324             elsif( $message->code == MSG_WATCH_CUSR ) {
325             my $from = $message->unpack_int();
326              
327             my $m = "cursor_prop_$prop";
328             my $cursor = $object->$m( $from );
329             my $id = $self->message_state->{next_cursorid}++;
330              
331             $self->peer_hascursor->{$id} = CursorObject( $cursor, $object );
332             $response = Tangence::Message->new( $self, MSG_WATCHING_CUSR )
333             ->pack_int( $id )
334             ->pack_int( 0 ) # first index
335             ->pack_int( $#{ $object->${\"get_prop_$prop"} } ) # last index
336             }
337             }
338 19         54 catch ( $e ) {
339             return $ctx->responderr( $e );
340             }
341              
342 18         85 $ctx->respond( $response );
343              
344 18 100       89 $self->_send_initial( $object, $prop ) if $want_initial;
345             }
346              
347 8         13 sub _send_initial ( $self, $object, $prop )
  8         27  
348 8     8   13 {
  8         11  
  8         13  
349 8         23 my $m = "get_prop_$prop";
350 8 50       45 return unless( $object->can( $m ) );
351              
352             try {
353             my $value = $object->$m();
354             my $message = $object->generate_message_UPDATE( $self, $prop, CHANGE_SET, $value );
355             $self->request(
356             request => $message,
357 8     8   31 on_response => sub { "IGNORE" },
358             );
359             }
360 8         21 catch ( $e ) {
361             warn "$e during initial property fetch";
362             }
363             }
364              
365 4         8 sub handle_request_UNWATCH ( $self, $token, $message )
  4         8  
366 4     4 0 10 {
  4         7  
  4         6  
367 4         30 my $ctx = Tangence::Server::Context->new( $self, $token );
368              
369 4         14 my $response;
370             try {
371             my $objid = $message->unpack_int();
372             my $prop = $message->unpack_str();
373              
374             my $object = $self->get_by_id( $objid );
375              
376             my $pdef = $object->can_property( $prop ) or
377             die "Object does not have property $prop\n";
378              
379             # Delete from watches and obtain id
380             my $id;
381             @{ $self->watches } = grep { $_->[0] == $object and $_->[1] eq $prop and ( $id = $_->[2], 0 ) or 1 }
382             @{ $self->watches };
383             defined $id or
384             die "Not watching $prop\n";
385              
386             $object->unwatch_property( $prop, $id );
387              
388             $response = Tangence::Message->new( $self, MSG_OK );
389             }
390 4         14 catch ( $e ) {
391             return $ctx->responderr( $e );
392             }
393              
394 4         36 $ctx->respond( $response );
395             }
396              
397 12         19 sub handle_request_CUSR_NEXT ( $self, $token, $message )
  12         15  
398 12     12 0 19 {
  12         16  
  12         16  
399 12         30 my $cursor_id = $message->unpack_int();
400              
401 12         63 my $ctx = Tangence::Server::Context->new( $self, $token );
402              
403 12 50       37 my $cursorobj = $self->peer_hascursor->{$cursor_id} or
404             return $ctx->responderr( "No such cursor with id $cursor_id" );
405              
406 12         83 $cursorobj->cursor->handle_request_CUSR_NEXT( $ctx, $message );
407             }
408              
409 6         13 sub handle_request_CUSR_DESTROY ( $self, $token, $message )
  6         8  
410 6     6 0 13 {
  6         11  
  6         9  
411 6         19 my $cursor_id = $message->unpack_int();
412              
413 6         42 my $ctx = Tangence::Server::Context->new( $self, $token );
414              
415 6         27 my $cursorobj = delete $self->peer_hascursor->{$cursor_id};
416 6         37 $self->drop_cursorobj( $cursorobj );
417              
418 6         41 $ctx->respond( Tangence::Message->new( $self, MSG_OK ) );
419             }
420              
421 6         11 sub drop_cursorobj ( $self, $cursorobj )
422 6     6 0 13 {
  6         9  
  6         19  
423 6         30 my $m = "uncursor_prop_" . $cursorobj->cursor->prop->name;
424 6         27 $cursorobj->obj->$m( $cursorobj->cursor );
425             }
426              
427 9         18 sub handle_request_INIT ( $self, $token, $message )
  9         15  
428 9     9 0 23 {
  9         20  
  9         26  
429 9         47 my $major = $message->unpack_int();
430 9         45 my $minor_max = $message->unpack_int();
431 9         31 my $minor_min = $message->unpack_int();
432              
433 9         108 my $ctx = Tangence::Server::Context->new( $self, $token );
434              
435 9 50       42 if( $major != VERSION_MAJOR ) {
436 0         0 return $ctx->responderr( "Major version $major not available" );
437             }
438              
439             # Don't accept higher than the minor version we recognise
440 9 50       43 $minor_max = VERSION_MINOR if $minor_max > VERSION_MINOR;
441 9 50       27 $minor_min = VERSION_MINOR_MIN if $minor_min < VERSION_MINOR_MIN;
442              
443 9 50       43 if( $minor_max < $minor_min ) {
444 0         0 return $ctx->responderr( "No suitable minor version available" );
445             }
446              
447             # For unit tests or other synchronous cases, we need to set the version
448             # -before- we send the message. But we'd better construct the response
449             # message before setting the version, in case it makes a difference.
450 9         59 my $response = Tangence::Message->new( $self, MSG_INITED )
451             ->pack_int( $major )
452             ->pack_int( $minor_max );
453              
454 9         87 $self->minor_version( $minor_max );
455              
456 9         47 $ctx->respond( $response );
457             }
458              
459 9         24 sub handle_request_GETROOT ( $self, $token, $message )
  9         16  
460 9     9 0 57 {
  9         20  
  9         23  
461 9         54 my $identity = TYPE_ANY->unpack_value( $message );
462              
463 9         612 my $ctx = Tangence::Server::Context->new( $self, $token );
464              
465 9         80 $self->identity( $identity );
466              
467 9         66 my $root = $self->rootobj( $identity );
468              
469 9         60 my $response = Tangence::Message->new( $self, MSG_RESULT );
470 9         67 TYPE_OBJ->pack_value( $response, $root );
471              
472 9         89 $ctx->respond( $response );
473             }
474              
475 9         19 sub handle_request_GETREGISTRY ( $self, $token, $message )
  9         18  
476 9     9 0 21 {
  9         39  
  9         25  
477 9         67 my $ctx = Tangence::Server::Context->new( $self, $token );
478              
479 9 50       67 $self->permit_registry or
480             return $ctx->responderr( "This client is not permitted access to the registry" );
481              
482 9         54 my $response = Tangence::Message->new( $self, MSG_RESULT );
483 9         67 TYPE_OBJ->pack_value( $response, $self->registry );
484              
485 9         43 $ctx->respond( $response );
486             }
487              
488             my %change_values = (
489             on_set => CHANGE_SET,
490             on_add => CHANGE_ADD,
491             on_del => CHANGE_DEL,
492             on_push => CHANGE_PUSH,
493             on_shift => CHANGE_SHIFT,
494             on_splice => CHANGE_SPLICE,
495             on_move => CHANGE_MOVE,
496             );
497              
498 38         59 sub _install_watch ( $self, $object, $prop )
  38         59  
499 38     38   56 {
  38         55  
  38         51  
500 38         119 my $pdef = $object->can_property( $prop );
501 38         132 my $dim = $pdef->dimension;
502              
503 38         144 weaken( my $weakself = $self );
504              
505 38         56 my %callbacks;
506 38         58 foreach my $name ( @{ CHANGETYPES->{$dim} } ) {
  38         119  
507 106         227 my $how = $change_values{$name};
508             $callbacks{$name} = set_subname "__WATCH($prop:$name)__" => sub {
509 37 50   37   109 $weakself or return;
510 37         97 my $object = shift;
511              
512 37         147 my $message = $object->generate_message_UPDATE( $weakself, $prop, $how, @_ );
513             $weakself->request(
514             request => $message,
515 37         149 on_response => sub { "IGNORE" },
516 37         219 );
517 106         950 };
518             }
519              
520 38         201 my $id = $object->watch_property( $prop, %callbacks );
521              
522 38         80 push @{ $self->watches }, [ $object, $prop, $id ];
  38         167  
523             }
524              
525 2         5 sub object_destroyed ( $self, $obj, @rest )
  2         4  
526 2     2 0 4 {
  2         4  
  2         3  
527 2 50       6 if( my $subs = $self->subscriptions ) {
528 2         6 my $i = 0;
529 2         8 while( $i < @$subs ) {
530 2         6 my $s = $subs->[$i];
531              
532 2 50       8 $i++, next unless $s->[0] == $obj;
533              
534 2         6 my ( undef, $event, $id ) = @$s;
535 2         9 $obj->unsubscribe_event( $event, $id );
536              
537 2         10 splice @$subs, $i, 1;
538             # No $i++
539             }
540             }
541              
542 2 50       6 if( my $watches = $self->watches ) {
543 2         4 my $i = 0;
544 2         8 while( $i < @$watches ) {
545 8         13 my $w = $watches->[$i];
546              
547 8 50       19 $i++, next unless $w->[0] == $obj;
548              
549 8         14 my ( undef, $prop, $id ) = @$w;
550 8         24 $obj->unwatch_property( $prop, $id );
551              
552 8         55 splice @$watches, $i, 1;
553             # No $i++
554             }
555             }
556              
557 2         16 $self->SUPER::object_destroyed( $obj, @rest );
558             }
559              
560             =head1 OVERRIDEABLE METHODS
561              
562             The following methods are provided but intended to be overridden if the
563             implementing class wishes to provide different behaviour from the default.
564              
565             =cut
566              
567             =head2 rootobj
568              
569             $rootobj = $server->rootobj( $identity )
570              
571             Invoked when a C message is received from the client, this method
572             should return a L as root object for the connection.
573              
574             The default implementation will return the object with ID 1; i.e. the first
575             object created in the registry.
576              
577             =cut
578              
579             sub rootobj
580             {
581 9     9 1 24 my $self = shift;
582              
583 9         38 return $self->registry->get_by_id( 1 );
584             }
585              
586             =head2 permit_registry
587              
588             $allow = $server->permit_registry
589              
590             Invoked when a C message is received from the client, this method
591             should return a boolean to indicate whether the client is allowed to access
592             the object registry.
593              
594             The default implementation always permits this, but an overridden method may
595             decide to disallow it in some situations. When disabled, a client will not be
596             able to gain access to any serverside objects other than the root object, and
597             (recursively) any other objects returned by methods, events or properties on
598             objects already known. This can be used as a security mechanism.
599              
600             =cut
601              
602 9     9 1 42 sub permit_registry { 1; }
603              
604             =head1 AUTHOR
605              
606             Paul Evans
607              
608             =cut
609              
610             0x55AA;