File Coverage

blib/lib/Yote/Server.pm
Criterion Covered Total %
statement 339 504 67.2
branch 85 178 47.7
condition 36 89 40.4
subroutine 49 66 74.2
pod 1 8 12.5
total 510 845 60.3


line stmt bran cond sub pod time code
1             package Yote::Server;
2              
3 12     12   387480 use strict;
  12         24  
  12         264  
4 12     12   48 use warnings;
  12         12  
  12         228  
5              
6 12     12   36 no warnings 'uninitialized';
  12         48  
  12         396  
7 12     12   36 no warnings 'numeric';
  12         12  
  12         204  
8              
9 12     12   4932 use Lock::Server;
  12         317412  
  12         516  
10 12     12   9492 use Yote;
  12         230076  
  12         288  
11              
12 12     12   84 use bytes;
  12         12  
  12         36  
13 12     12   8412 use IO::Socket::SSL;
  12         465180  
  12         84  
14 12     12   1968 use JSON;
  12         24  
  12         72  
15 12     12   1068 use Time::HiRes qw(time);
  12         12  
  12         132  
16 12     12   5460 use URI::Escape;
  12         11400  
  12         588  
17 12     12   5184 use UUID::Tiny;
  12         93108  
  12         1020  
18              
19              
20 12     12   72 use vars qw($VERSION);
  12         12  
  12         27888  
21              
22             $VERSION = '1.26';
23              
24             our $DEBUG = 0;
25              
26             sub new {
27 12     12 0 10224 my( $pkg, $args ) = @_;
28 12   33     120 my $class = ref( $pkg ) || $pkg;
29             my $server = bless {
30             args => $args || {},
31              
32             # the following are the args currently used
33             yote_root_dir => $args->{yote_root_dir},
34             yote_host => $args->{yote_host} || '127.0.0.1',
35             yote_port => $args->{yote_port} || 8881,
36             pids => [],
37             _locker => new Lock::Server( {
38             port => $args->{lock_port},
39             host => $args->{lock_host} || '127.0.0.1',
40             lock_attempt_timeout => $args->{lock_attempt_timeout},
41             lock_timeout => $args->{lock_timeout},
42             } ),
43 12   50     372 STORE => Yote::ServerStore->_new( { root => $args->{yote_root_dir} } ),
      50        
      50        
      50        
44             }, $class;
45 12         120 $server->{STORE}{_locker} = $server->{_locker};
46 12         48 $server;
47             } #new
48              
49             sub store {
50 0     0 0 0 shift->{STORE};
51             }
52              
53             sub load_options {
54              
55 0     0 0 0 my( $yote_root_dir ) = @_;
56              
57 0         0 my $confile = "$yote_root_dir/yote.conf";
58              
59             #
60             # set up default options
61             #
62 0         0 my $options = {
63             yote_root_dir => $yote_root_dir,
64             yote_host => '127.0.0.1',
65             yote_port => 8881,
66             lock_port => 8004,
67             lock_host => '127.0.0.1',
68             lock_attempt_timeout => 12,
69             lock_timeout => 10,
70             use_ssl => 0,
71             SSL_cert_file => '',
72             SSL_key_file => '',
73             };
74              
75             #
76             # override base defaults with those from conf file
77             #
78 0 0 0     0 if( -f $confile && -r $confile ) {
79             # TODO - create conf with defaults and make it part of the install
80 0 0       0 open( IN, "<$confile" ) or die "Unable to open config file $@ $!";
81 0         0 while( ) {
82 0         0 chomp;
83 0         0 s/\#.*//;
84 0 0       0 if( /^\s*([^=\s]+)\s*=\s*([^\s].*)\s*$/ ) {
85 0 0       0 if( defined $options->{$1} ) {
86 0 0       0 $options->{$1} = $2 if defined $options->{$1};
87             } else {
88 0         0 print STDERR "Warning: encountered '$1' in file. Ignoring";
89             }
90             }
91             }
92 0         0 close IN;
93             } #if config file is there
94              
95 0         0 return $options;
96             } #load_options
97              
98             sub ensure_locker {
99 0     0 0 0 my $self = shift;
100             # if running as the server, this will not be called.
101             # if something else is managing forking ( like the CGI )
102             # this should be run to make sure the locker socket
103             # opens and closes
104             $SIG{INT} = sub {
105 0     0   0 _log( "$0 got INT signal. Shutting down." );
106 0 0       0 $self->{_locker}->stop if $self->{_locker};
107 0         0 exit;
108 0         0 };
109              
110 0 0       0 if( ! $self->{_locker}->ping(1) ) {
111 0         0 $self->{_locker}->start;
112             }
113             } #ensure_locker
114              
115             sub start {
116 12     12 0 37944 my $self = shift;
117              
118 12         84 $self->{_locker}->start;
119              
120 11         14036 my $listener_socket = $self->_create_listener_socket;
121 11 50       132 die "Unable to open socket " unless $listener_socket;
122              
123 11 100       6466 if( my $pid = fork ) {
124             # parent
125 1         30 $self->{server_pid} = $pid;
126 1         26 return $pid;
127             }
128              
129             # in child
130 10         570 $0 = "YoteServer process";
131 10         1370 $self->_run_loop( $listener_socket );
132              
133             } #start
134              
135             sub stop {
136 1     1 0 59678 my $self = shift;
137 1 50       5 if( my $pid = $self->{server_pid} ) {
138 1         8 $self->{error} = "Sending INT signal to lock server of pid '$pid'";
139 1         23 kill 'INT', $pid;
140 1         3 return 1;
141             }
142 0         0 $self->{error} = "No Yote server running";
143 0         0 return 0;
144             }
145              
146              
147              
148             =head2 run
149              
150             Runs the lock server.
151              
152             =cut
153             sub run {
154 0     0 1 0 my $self = shift;
155 0         0 my $listener_socket = $self->_create_listener_socket;
156 0 0       0 die "Unable to open socket " unless $listener_socket;
157 0         0 $self->_run_loop( $listener_socket );
158             }
159              
160             sub _create_listener_socket {
161 11     11   88 my $self = shift;
162              
163 11         77 my $listener_socket;
164 11         11 my $count = 0;
165              
166 11 50 0     209 if( $self->{use_ssl} && ( ! $self->{SSL_cert_file} || ! $self->{SSL_key_file} ) ) {
      33        
167 0         0 die "Cannot start server. SSL selected but is missing filename for SSL_cert_file and/or SSL_key_file";
168             }
169 11   33     286 while( ! $listener_socket && ++$count < 10 ) {
170 11 50       165 if( $self->{args}{use_ssl} ) {
171 0         0 my $cert_file = $self->{args}{SSL_cert_file};
172 0         0 my $key_file = $self->{args}{SSL_key_file};
173 0 0       0 if( index( $cert_file, '/' ) != 0 ) {
174 0         0 $cert_file = "$self->{yote_root_dir}/$cert_file";
175             }
176 0 0       0 if( index( $key_file, '/' ) != 0 ) {
177 0         0 $key_file = "$self->{yote_root_dir}/$key_file";
178             }
179 0         0 $listener_socket = new IO::Socket::SSL(
180             Listen => 10,
181             LocalAddr => "$self->{yote_host}:$self->{yote_port}",
182             SSL_cert_file => $cert_file,
183             SSL_key_file => $key_file,
184             );
185             } else {
186 11         539 $listener_socket = new IO::Socket::INET(
187             Listen => 10,
188             LocalAddr => "$self->{yote_host}:$self->{yote_port}",
189             );
190             }
191 11 50       4367 last if $listener_socket;
192            
193 0         0 print STDERR "Unable to open the yote socket [$self->{yote_host}:$self->{yote_port}] ($!). Retry $count of 10\n";
194 0         0 sleep 5 * $count;
195             }
196              
197 11 50       88 unless( $listener_socket ) {
198 0         0 $self->{error} = "Unable to open yote socket on port '$self->{yote_port}' : $! $@\n";
199 0         0 $self->{_locker}->stop;
200 0         0 _log( "unable to start yote server : $@ $!." );
201 0         0 return 0;
202             }
203              
204 11         495 print STDERR "Starting yote server\n";
205              
206 11 50       143 unless( $self->{yote_root_dir} ) {
207 0         0 eval('use Yote::ConfigData');
208 0 0       0 $self->{yote_root_dir} = $@ ? '/opt/yote' : Yote::ConfigData->config( 'yote_root' );
209 0         0 undef $@;
210             }
211              
212             # if this is cancelled, make sure all child procs are killed too
213             $SIG{INT} = sub {
214 1     1   16724 _log( "got INT signal. Shutting down." );
215 1 50       30 $listener_socket && $listener_socket->close;
216 1         63 for my $pid ( @{ $self->{_pids} } ) {
  1         14  
217 12         88 kill 'HUP', $pid;
218             }
219 1         47 $self->{_locker}->stop;
220 1         1730 exit;
221 11         264 };
222              
223 11         132 $SIG{CHLD} = 'IGNORE';
224              
225 11         22 return $listener_socket;
226             } #_create_listener_socket
227              
228             sub _run_loop {
229 10     10   70 my( $self, $listener_socket ) = @_;
230 10         620 while( my $connection = $listener_socket->accept ) {
231 57         12605700 $self->_process_request( $connection );
232             }
233             }
234              
235             sub _log {
236 1     1   5 my( $msg, $sev ) = @_;
237 1   50     49 $sev //= 1;
238 1 50       8 if( $sev <= $DEBUG ) {
239 0         0 print STDERR "Yote::Server : $msg\n";
240 0 0       0 open my $out, ">>/opt/yote/log/yote.log" or return;
241 0         0 print $out "$msg\n";
242 0         0 close $out;
243             }
244             }
245              
246             sub _find_ids_in_data {
247 6     6   10 my $data = shift;
248 6         16 my $r = ref( $data );
249 6 50       23 if( $r eq 'ARRAY' ) {
    0          
    0          
250 6 50       14 return grep { $_ && index($_,'v')!=0 } map { ref( $_ ) ? _find_ids_in_data($_) : $_ } @$data;
  7 50       99  
  7         24  
251             }
252             elsif( $r eq 'HASH' ) {
253 0 0       0 return grep { $_ && index($_,'v')!=0} map { ref( $_ ) ? _find_ids_in_data($_) : $_ } values %$data;
  0 0       0  
  0         0  
254             }
255             elsif( $r ) {
256 0         0 die "_find_ids_in_data encountered a non ARRAY or HASH reference";
257             }
258             } #_find_ids_in_data
259              
260             # EXPERIMETNAL - this will return the entire public tree. The idea is to program
261             # without having to explicitly shove data across. This errs on the side of much
262             # more data, so relies on private data and method calls (encapsulation) to
263             # mitigate this
264              
265             sub _unroll_ids {
266 11     11   95 my( $store, $ids, $seen ) = @_;
267 11   100     81 $seen //= {};
268              
269 11         20 my( @items ) = ( map { $store->fetch($_) } @$ids );
  21         2874  
270              
271 11         1622 my @outids;
272 11         84 for my $item( @items ) {
273 21         77 my $iid = $store->_get_id($item);
274 21         249 my $r = ref( $item );
275 21         30 $seen->{$iid}++;
276 21 100       49 if( $r eq 'ARRAY' ) {
    100          
277 6         47 push @outids, grep { ! $seen->{$_}++ } map { $store->_get_id($_) } grep { ref($_) } @$item;
  6         91  
  6         21  
  18         80  
278             }
279             elsif( $r eq 'HASH' ) {
280 4         159 push @outids, grep { ! $seen->{$_}++ } map { $store->_get_id($_) } grep { ref($_) } values %$item;
  4         68  
  4         375  
  8         563  
281             }
282             else {
283 11         16 my $data = $item->{DATA};
284 11 100 100     32 push @outids, map { $data->{$_} } grep { /^[^_]/ && $data->{$_} != /^v/ && ! $seen->{$data->{$_}}++ } keys %$data;
  10         32  
  31         225  
285             }
286             }
287              
288 11 100       85 _unroll_ids( $store, \@outids, $seen ) if @outids;
289              
290              
291 11         78 [ keys %$seen ];
292             } #_unroll_ids
293              
294             sub _process_request {
295             #
296             # Reads incomming request from the socket, parses it, performs it and
297             # prints the result back to the socket.
298             #
299 57     57   289 my( $self, $sock ) = @_;
300              
301              
302 57 100       121996 if ( my $pid = fork ) {
303             # parent
304 48         497 push @{$self->{_pids}},$pid;
  48         26124  
305             } else {
306             # use Devel::SimpleProfiler;Devel::SimpleProfiler::start;
307 9         371 my( $self, $sock ) = @_;
308             #child
309 9         642 $0 = "YoteServer processing request";
310             $SIG{INT} = sub {
311 0     0   0 _log( " process $$ got INT signal. Shutting down." );
312 0         0 $sock->close;
313 0         0 exit;
314 9         1120 };
315            
316            
317 9         1311 my $req = <$sock>;
318 9         437 $ENV{REMOTE_HOST} = $sock->peerhost;
319 9         2411 my( %headers, %cookies );
320 9         486 while( my $hdr = <$sock> ) {
321 27         536 $hdr =~ s/\s*$//s;
322 27 100       198 last if $hdr !~ /[a-zA-Z]/;
323 18         154 my( $key, $val ) = ( $hdr =~ /^([^:]+):(.*)/ );
324 18         259 $headers{$key} = $val;
325             }
326              
327 9         164 for my $cookie ( split( /\s*;\s*/, $headers{Cookie} ) ) {
328 0         0 $cookie =~ s/^\s*|^\s*$//g;
329 0         0 my( $key, $val ) = split( /\s*=\s*/, $cookie, 2 );
330 0         0 $cookies{ $key } = $val;
331             }
332            
333             #
334             # read certain length from socket ( as many bytes as content length )
335             #
336 9         87 my $content_length = $headers{'Content-Length'};
337 9         29 my $data;
338 9 50 33     407 if ( $content_length > 0 && ! eof $sock) {
339 9         111 read $sock, $data, $content_length;
340             }
341 9         195 my( $verb, $path ) = split( /\s+/, $req );
342              
343             # escape for serving up web pages
344             # the thought is that this should be able to be a stand alone webserver
345             # for testing and to provide the javascript
346 9 50       39 if ( $path =~ m!/__/! ) {
347             # TODO - make sure install script makes the directories properly
348 0         0 my $filename = "$self->{yote_root_dir}/html/" . substr( $path, 4 );
349 0 0       0 if ( -e $filename ) {
350 0         0 my @stat = stat $filename;
351              
352 0 0       0 my $content_type = $filename =~ /css$/ ? 'text/css' : 'text/html';
353 0         0 my @headers = (
354             "Content-Type: $content_type; charset=utf-8",
355             'Server: Yote',
356             "Content-Length: $stat[7]",
357             );
358              
359 0         0 open( IN, "<$filename" );
360              
361 0         0 $sock->print( "HTTP/1.1 200 OK\n" . join ("\n", @headers). "\n\n" );
362              
363 0         0 while ( $data = ) {
364 0         0 $sock->print( $data );
365             }
366 0         0 close IN;
367             } else {
368 0         0 $sock->print( "HTTP/1.1 404 FILE NOT FOUND\n\n" );
369             }
370 0         0 $sock->close;
371 0         0 exit;
372             }
373            
374              
375             # data has the input parmas in JSON format.
376             # POST /
377              
378 9 50       45 if ( $verb ne 'POST' ) {
379 0         0 $sock->print( "HTTP/1.1 400 BAD REQUEST\n\n" );
380 0         0 $sock->close;
381             }
382              
383 9         70 $data =~ s/^p=//;
384 9         21 my $out_json;
385 9         71 eval {
386 9         269 $out_json = $self->invoke_payload( $data );
387             };
388              
389 9 50       182 if( ref $@ eq 'HASH' ) {
    100          
390 0         0 $out_json = encode_json( $@ );
391             }
392             elsif( $@ ) {
393 3         77 $out_json = encode_json( {
394             err => $@,
395             } );
396             }
397 9         301 my @headers = (
398             'Content-Type: text/json; charset=utf-8',
399             'Server: Yote',
400             'Access-Control-Allow-Headers: accept, content-type, cookie, origin, connection, cache-control',
401             'Access-Control-Allow-Origin: *', #TODO - have this configurable
402             'Content-Length: ' . bytes::length( $out_json ),
403             );
404            
405 9         13061 $sock->print( "HTTP/1.1 200 OK\n" . join ("\n", @headers). "\n\n$out_json\n" );
406            
407 9         1695 $sock->close;
408              
409 9         9168 exit;
410              
411             } #child
412             } #_process_request
413             sub invoke_payload {
414 9     9 0 25 my( $self, $raw_req_data, $file_uploads ) = @_;
415              
416 9         457 my $req_data = decode_json( $raw_req_data );
417            
418 9         89 my( $obj_id, $token, $action, $params ) = @$req_data{ 'i', 't', 'a', 'pl' };
419            
420 9         244 my $server_root = $self->{STORE}->fetch_server_root;
421 9         334 my $server_root_id = $server_root->{ID};
422            
423              
424 9         21 my $id_to_last_update_time;
425 9 100 66     304 my $session = $token && $token ne '_' ? $server_root->_fetch_session( $token ) : undef;
426              
427 9 100       60 if( $session ) {
428 4         109 $id_to_last_update_time = $session->get__has_ids2times;
429             }
430              
431 9 0 66     1562 unless( $obj_id eq '_' || # either the object id that is acted upon is
      33        
432             $obj_id eq $server_root_id || # the server root or is known to the session
433             ( $id_to_last_update_time->{$obj_id} ) ) {
434             # tried to do an action on an object it wasn't handed. do a 404
435 0         0 die( "client with token [$token] and session ($session) tried to invoke on obj id '$obj_id' which it does not have" );
436             }
437 9 100 66     159 if( substr( $action, 0, 1 ) eq '_' || $action =~ /^[gs]et$/ ) {
438 1         38 die( "Private method called" );
439             }
440              
441 8 50 33     98 if ( $params && ref( $params ) ne 'ARRAY' ) {
442 0         0 die( "Bad Req Param Not Array : $params" );
443             }
444              
445 8         23 my $store = $self->{STORE};
446              
447             # now things are getting a bit more complicated. The params passed in
448             # are always a list, but they may contain other containers that are not
449             # yote objects. So, transform the incomming parameter list and check all
450             # yote objects inside for may. Use a recursive helper function for this.
451 8         81 my $in_params = $store->__transform_params( $params, $session, $file_uploads );
452              
453             #
454             # This obj is the object that the method call is on
455             #
456 8 100       100 my $obj = $obj_id eq '_' ? $server_root :
457             $store->fetch( $obj_id );
458              
459 8 100       8384 unless( $obj->can( $action ) ) {
460 2         75 die( "Bad Req : invalid method :'$action'" );
461             }
462              
463             # if there is a session, attach it to the object
464 6 100       20 if( $session ) {
465 2         15 $obj->{SESSION} = $session;
466 2         5 $obj->{SESSION}{SERVER_ROOT} = $server_root;
467              
468             }
469              
470             #
471             # <<------------- the actual method call --------------->>
472             #
473 6         263 my(@res) = ($obj->$action( @$in_params ));
474              
475             #
476             # this is included in what is returned to the client
477             #
478 6         78 my $out_res = $store->_xform_in( \@res, 'allow datastructures' );
479              
480             #
481             # in case the method generated a new session, (re)set that now
482             #
483 6         65 $session = $obj->{SESSION};
484 6 100       17 if( $session ) {
485 2         16 $id_to_last_update_time = $session->get__has_ids2times;
486             }
487            
488             # the ids that were referenced explicitly in the
489             # method call.
490 6         74 my @out_ids = _find_ids_in_data( $out_res );
491              
492             #
493             # Based on the return value of the method call,
494             # these ids are ones that the client should have.
495             # We will check to see if these need updates
496             #
497 6         12 my @should_have = ( @{ _unroll_ids( $store, [@out_ids, keys %$id_to_last_update_time] ) } );
  6         65  
498 6         19 my( @updates, %methods );
499              
500             #
501             # check if existing are in the session
502             #
503 6         37 for my $should_have_id ( @should_have, keys %$id_to_last_update_time ) {
504 24         567 my $needs_update = 1;
505            
506 24 100       73 if( $session) {
507             #
508             # check if the client of this session needs an update, otherwise assume that it does
509             #
510 18 100       103 my( $client_s, $client_ms ) = @{ $id_to_last_update_time->{$should_have_id} || [] };
  18         212  
511 18         140 my( $server_s, $server_ms ) = $store->_last_updated( $should_have_id );
512              
513 18   66     134 $needs_update = $client_s == 0 || $server_s > $client_s || ($server_s == $client_s && $server_ms > $client_ms );
514             }
515              
516 24 100       57 if( $needs_update ) {
517 12         52 my $should_have_obj = $store->fetch( $should_have_id );
518 12         1738 my $ref = ref( $should_have_obj );
519 12         10 my $data;
520 12 100       37 if( $ref eq 'ARRAY' ) {
    100          
521 4         11 $data = [ map { $store->_xform_in( $_ ) } @$should_have_obj ];
  12         752  
522             } elsif( $ref eq 'HASH' ) {
523 2         11 $data = { map { $_ => $store->_xform_in( $should_have_obj->{$_} ) } keys %$should_have_obj };
  4         78  
524             } else {
525 6         10 my $d = $should_have_obj->{DATA};
526 10         102 $data = { map { $_ => $d->{$_} } grep { index($_,"_") != 0 } keys %$d },
  20         33  
527 6   66     25 $methods{$ref} ||= $should_have_obj->_callable_methods;
528             }
529 12         100 my $update = {
530             id => $should_have_id,
531             cls => $ref,
532             data => $data,
533             };
534 12         19 push @updates, $update;
535 12 100       41 if( $session ) {
536 6         78 $id_to_last_update_time->{$should_have_id} = [Time::HiRes::gettimeofday];
537             }
538             } # if this needs an update
539            
540             } #each object the client should have
541              
542              
543 6         200 my $out_json = to_json( { result => $out_res,
544             updates => \@updates,
545             methods => \%methods,
546             } );
547              
548 6         250 delete $obj->{SESSION};
549 6         81 $self->{STORE}->stow_all;
550            
551 6         6548 return $out_json;
552             } #invoke_payload
553              
554             # ------- END Yote::Server
555              
556             package Yote::ServerStore;
557              
558 12     12   168 use Data::RecordStore;
  12         12  
  12         240  
559              
560 12     12   60 use base 'Yote::ObjStore';
  12         12  
  12         13356  
561              
562             sub _new { #Yote::ServerStore
563 12     12   420 my( $pkg, $args ) = @_;
564 12         60 $args->{store} = "$args->{root}/DATA_STORE";
565 12         156 my $self = $pkg->SUPER::_new( $args );
566              
567             # keeps track of when any object had been last updated.
568             # use like $self->{OBJ_UPDATE_DB}->put_record( $obj_id, [ time ] );
569             # or my( $time ) = @{ $self->{OBJ_UPDATE_DB}->get_record( $obj_id ) };
570 12         14988 $self->{OBJ_UPDATE_DB} = Data::RecordStore::FixedStore->open( "LL", "$args->{root}/OBJ_META" );
571              
572 12         1716 my( $m, $ms ) = ( Time::HiRes::gettimeofday );
573 12         108 $self->{OBJ_UPDATE_DB}->put_record( $self->{ID}, [ $m, $ms ] );
574              
575 12         1464 $self;
576             } #_new
577              
578             sub _dirty {
579 398     398   61620 my( $self, $ref, $id ) = @_;
580 398         1004 $self->SUPER::_dirty( $ref, $id );
581 398         1941 $self->{OBJ_UPDATE_DB}->ensure_entry_count( $id );
582              
583 398         45571 my( $m, $ms ) = ( Time::HiRes::gettimeofday );
584 398         1642 $self->{OBJ_UPDATE_DB}->put_record( $id, [ $m, $ms ] );
585             }
586              
587             sub stow_all {
588 31     31   2000288 my $self = $_[0];
589 31         50 for my $obj (values %{$self->{_DIRTY}} ) {
  31         205  
590 167         10717 my $obj_id = $self->_get_id( $obj );
591 167         2057 $self->{OBJ_UPDATE_DB}->ensure_entry_count( $obj_id );
592             }
593 31         2054 $self->SUPER::stow_all;
594             } #stow_all
595              
596             sub _last_updated {
597 18     18   24 my( $self, $obj_id ) = @_;
598 18         17 my( $s, $ms ) = @{ $self->{OBJ_UPDATE_DB}->get_record( $obj_id ) };
  18         66  
599 18         1061 $s, $ms;
600             }
601              
602             sub _log {
603 0     0   0 Yote::Server::_log(shift);
604             }
605              
606              
607             sub __transform_params {
608             #
609             # Recursively transforms incoming parameters into values, yote objects, or non yote containers.
610             # This checks to make sure that the parameters are allowed by the given token.
611             # Throws execptions if the parametsr are not allowed, or if a reference that is not a hash or array
612             # is encountered.
613             #
614 8     8   27 my( $self, $param, $session, $files ) = @_;
615              
616 8 50       99 if( ref( $param ) eq 'HASH' ) {
    50          
    0          
617 0         0 return { map { $_ => $self->__transform_params($param->{$_}, $session, $files) } keys %$param };
  0         0  
618             }
619             elsif( ref( $param ) eq 'ARRAY' ) {
620 8         32 return [ map { $self->__transform_params($_, $session, $files) } @$param ];
  0         0  
621             } elsif( ref( $param ) ) {
622 0         0 die "Transforming Params: got weird ref '" . ref( $param ) . "'";
623             }
624 0 0 0     0 if( ( index( $param, 'v' ) != 0 && index($param, 'f' ) != 0 ) && !$session->get__has_ids2times({})->{$param} ) {
      0        
625             # obj id given, but the client should not have that id
626 0 0       0 if( $param ) {
627 0         0 die { err => 'Sync Error', needs_resync => 1 };
628             }
629 0         0 return undef;
630             }
631 0         0 return $self->_xform_out( $param, $files );
632             } #__transform_params
633              
634             sub _xform_out {
635 265     265   1056706 my( $self, $val, $files ) = @_;
636 265 100       768 return undef unless defined( $val );
637 246 50       776 if( index($val,'f') == 0 ) {
638             # convert to file object
639 0 0       0 if( $val =~ /^f(\d+)_(\d+)$/ ) {
640 0         0 my( $offset_start, $offset_end ) = ( $1, $2 );
641 0         0 for( my $i=$offset_start; $i < $offset_end; $i++ ) {
642 0         0 my $file = $files->[$i];
643 0 0       0 if( $file ) {
644 0         0 my( $orig_filename ) = ( $file =~ /([^\/]*)$/ );
645 0         0 my( $extension ) = ( $orig_filename =~ /\.([^.\/]+)$/ );
646            
647             # TODO - cleanup, maybe use File::Temp or something
648 0         0 my $newname = "/tmp/".UUID::Tiny::create_uuid_as_string();
649 0         0 open (FILE, ">$newname");
650 0         0 my $fh = $file->fh;
651 0         0 while (read ($fh, my $Buffer, 1024)) {
652 0         0 print FILE $Buffer;
653             }
654 0         0 close FILE;
655             # create yote object here that wraps the file name
656 0         0 return $self->newobj( {
657             file_path => $newname,
658             file_extension => $extension,
659             file_name => $orig_filename,
660             } );
661             }
662             } #finding the file
663 0         0 return undef;
664             }
665             }
666 246         780 return $self->SUPER::_xform_out( $val );
667             } #_xform_out
668              
669              
670             #
671             # Unlike the superclass version of this, this provides an arguemnt to
672             # allow non-yote datastructures to be returned. The contents of those
673             # data structures will all recursively be xformed in.
674             #
675             sub _xform_in {
676 276     276   143582 my( $self, $val, $allow_datastructures ) = @_;
677              
678 276         1582 my $r = ref $val;
679 276 100       616 if( $r ) {
680 179 100       367 if( $allow_datastructures) {
681             # check if this is a yote object
682 8 100 66     238 if( ref( $val ) eq 'ARRAY' && ! tied( @$val ) ) {
    50 33        
683 6 100       32 return [ map { ref $_ ? $self->_xform_in( $_, $allow_datastructures ) : "v$_" } @$val ];
  7         47  
684             }
685             elsif( ref( $val ) eq 'HASH' && ! tied %$val ) {
686 0 0       0 return { map { $_ => ( ref( $val->{$_} ) ? $self->_xform_in( $val->{$_}, $allow_datastructures ) : "v$val->{$_}" ) } keys %$val };
  0         0  
687             }
688             }
689 173         394 return $self->_get_id( $val );
690             }
691              
692 97 50       370 return defined $val ? "v$val" : undef;
693             } #_xform_in
694              
695             sub newobj {
696 25     25   197 my( $self, $data, $class ) = @_;
697 25   100     175 $class ||= 'Yote::ServerObj';
698 25         281 $class->_new( $self, $data );
699             } #newobj
700              
701             sub fetch_server_root {
702 21     21   1782 my $self = shift;
703              
704 21 100       1525 return $self->{SERVER_ROOT} if $self->{SERVER_ROOT};
705              
706 12         120 my $system_root = $self->fetch_root;
707 12         20124 my $server_root = $system_root->get_server_root;
708 12 50       84 unless( $server_root ) {
709 12         156 $server_root = Yote::ServerRoot->_new( $self );
710 12         360 $system_root->set_server_root( $server_root );
711 12         852 $self->stow_all;
712             }
713              
714             # some setup here? accounts/webapps/etc?
715             # or make it simple. if the webapp has an account, then pass that account
716             # with the rest of the arguments
717              
718             # then verify if the command can run on the app object with those args
719             # or even : $myapp->run( 'command', @args );
720              
721 12   33     71316 $self->{SERVER_ROOT} ||= $server_root;
722              
723 12         108 $server_root;
724            
725             } #fetch_server_root
726              
727             sub lock {
728 5     5   33 my( $self, $key ) = @_;
729 5   33     389 $self->{_lockerClient} ||= $self->{_locker}->client( $$ );
730 5         1644 $self->{_lockerClient}->lock( $key );
731             }
732              
733             sub unlock {
734 5     5   36 my( $self, $key ) = @_;
735 5         74 $self->{_lockerClient}->unlock( $key );
736             }
737              
738              
739             # ------- END Yote::ServerStore
740              
741             package Yote::ServerObj;
742              
743 12     12   60 use base 'Yote::Obj';
  12         36  
  12         3708  
744              
745             sub _log {
746 0     0   0 Yote::Server::_log(shift);
747             }
748              
749             sub _err {
750 0     0   0 shift; #self
751 0         0 die { err => shift };
752             }
753              
754             $Yote::ServerObj::PKG2METHS = {};
755             sub __discover_methods {
756 6     6   21 my $pkg = shift;
757 6         10 my $meths = $Yote::ServerObj::PKG2METHS->{$pkg};
758 6 50       16 if( $meths ) {
759 0         0 return $meths;
760             }
761              
762 12     12   60 no strict 'refs';
  12         12  
  12         4884  
763 6         6 my @m = grep { $_ !~ /::/ } keys %{"${pkg}\::"};
  135         178  
  6         155  
764 6 100       23 if( $pkg eq 'Yote::ServerObj' ) { #the base, presumably
765 4         5 return [ sort grep { $_ !~ /^(_|[gs]et_|(can|[sg]et|VERSION|AUTOLOAD|DESTROY|CARP_TRACE|BEGIN|isa|import|PKG2METHS|ISA|add_to_|remove_from)$)/ } @m ];
  72         152  
766             }
767              
768 2         6 my %hasm = map { $_ => 1 } @m;
  63         101  
769 2         10 for my $class ( @{"${pkg}\::ISA" } ) {
  2         24  
770 2 50 33     17 next if $class eq 'Yote::ServerObj' || $class eq 'Yote::Obj';
771 0         0 my $pm = __discover_methods( $class );
772 0         0 push @m, @$pm;
773             }
774            
775 2         18 my $base_meths = __discover_methods( 'Yote::ServerObj' );
776 2         4 my( %base ) = map { $_ => 1 } 'AUTOLOAD', @$base_meths;
  6         12  
777 2   66     4 $meths = [ sort grep { $_ !~ /^(_|[gs]et_|(can|[sg]et|VERSION|AUTOLOAD|DESTROY|BEGIN|isa|import|PKG2METHS|ISA|add_to_|remove_from)$)/ && ! $base{$_} } @m ];
  63         172  
778 2         15 $Yote::ServerObj::PKG2METHS->{$pkg} = $meths;
779            
780 2         17 $meths;
781             } #__discover_methods
782              
783             # when sending objects across, the format is like
784             # id : { data : { }, methods : [] }
785             # the methods exclude all the methods of Yote::Obj
786             sub _callable_methods {
787 4     4   6 my $self = shift;
788 4         9 my $pkg = ref( $self );
789 4         11 __discover_methods( $pkg );
790             } # _callable_methods
791              
792              
793             sub _get {
794 0     0   0 my( $self, $fld, $default ) = @_;
795 0 0 0     0 if( ! defined( $self->{DATA}{$fld} ) && defined($default) ) {
796 0 0       0 if( ref( $default ) ) {
797 0         0 $self->{STORE}->_dirty( $default, $self->{STORE}->_get_id( $default ) );
798             }
799 0         0 $self->{STORE}->_dirty( $self, $self->{ID} );
800 0         0 $self->{DATA}{$fld} = $self->{STORE}->_xform_in( $default );
801             }
802 0         0 $self->{STORE}->_xform_out( $self->{DATA}{$fld} );
803             } #_get
804              
805              
806             # ------- END Yote::ServerObj
807              
808             package Yote::ServerRoot;
809              
810 12     12   48 use base 'Yote::ServerObj';
  12         12  
  12         9612  
811              
812             sub _init {
813 12     12   1296 my $self = shift;
814 12         216 $self->set__doesHave_Token2objs({});
815 12         360 $self->set__apps({});
816 12         324 $self->set__token_timeslots([]);
817 12         420 $self->set__token_timeslots_metadata([]);
818 12         336 $self->set__token_mutex([]);
819             }
820              
821             sub _log {
822 0     0   0 Yote::Server::_log(shift);
823             }
824              
825             #
826             # fetches or creates session which has a _token field
827             #
828             sub fetch_session {
829 0     0   0 my( $self, $token ) = @_;
830 0   0     0 my $session = $self->_fetch_session( $token ) || $self->_create_session;
831 0         0 $self->{SESSION} = $session;
832 0         0 $session;
833             }
834              
835             sub _fetch_session {
836 4     4   12 my( $self, $token ) = @_;
837            
838 4         42 $self->{STORE}->lock( 'token_mutex' );
839 4         8399 my $slots = $self->get__token_timeslots();
840              
841 4         2502 for( my $i=0; $i<@$slots; $i++ ) {
842 4 50       123 if( my $session = $slots->[$i]{$token} ) {
843 4 50       5658 if( $i > 0 ) {
844             # make sure this is in the most current 'boat'
845 0         0 $slots->[0]{ $token } = $session;
846             }
847 4         33 $self->{STORE}->unlock( 'token_mutex' );
848 4         5316 return $session;
849             }
850             }
851 0         0 $self->{STORE}->unlock( 'token_mutex' );
852 0         0 0;
853             } #_fetch_sesion
854              
855             sub _create_session {
856 1     1   2 my $self = shift;
857 1         1 my $tries = shift;
858              
859 1 50       11 if( $tries > 3 ) {
860 0         0 die "Error creating token. Got the same random number 4 times in a row";
861             }
862              
863 1         13 my $token = int( rand( 1_000_000_000 ) ); #TODO - find max this can be for long int
864            
865             # make the token boat. tokens last at least 10 mins, so quantize
866             # 10 minutes via time 10 min = 600 seconds = 600
867             # or easy, so that 1000 seconds ( ~ 16 mins )
868             # todo - make some sort of quantize function here
869 1         9 my $current_time_chunk = int( time / 100 );
870 1         2 my $earliest_valid_time_chunk = $current_time_chunk - 7;
871              
872 1         7 $self->{STORE}->lock( 'token_mutex' );
873              
874             #
875             # A list of slot 'boats' which store token -> ip
876             #
877 1         7151 my $slots = $self->get__token_timeslots();
878             #
879             # a list of times. the list index of these times corresponds
880             # to the slot 'boats'
881             #
882 1         344 my $slot_data = $self->get__token_timeslots_metadata();
883            
884             #
885             # Check if the token is already used ( very unlikely ).
886             # If already used, try this again :/
887             #
888 1         194 for( my $i=0; $i<@$slot_data; $i++ ) {
889 0 0       0 return $self->_create_session( $tries++ ) if $slots->[ $i ]{ $token };
890             }
891              
892             #
893             # See if the most recent time slot is current. If it is behind, create a new current slot
894             # create a new most recent boat.
895             #
896             my $session = $self->{STORE}->newobj( {
897 1         24 _has_ids2times => {},
898             _token => $token }, 'Yote::ServerSession' );
899            
900 1 50       80 if( $slot_data->[ 0 ] == $current_time_chunk ) {
901 0         0 $slots->[ 0 ]{ $token } = $session;
902             } else {
903 1         12 unshift @$slot_data, $current_time_chunk;
904 1         5 unshift @$slots, { $token => $session };
905             }
906            
907             #
908             # remove this token from old boats so it doesn't get purged
909             # when in a valid boat.
910             #
911 1         101 for( my $i=1; $i<@$slot_data; $i++ ) {
912 0         0 delete $slots->[$i]{ $token };
913             }
914              
915 1         14 $self->{STORE}->_stow( $slots );
916 1         242 $self->{STORE}->_stow( $slot_data );
917 1         873 $self->{STORE}->unlock( 'token_mutex' );
918              
919              
920 1         1523 $session;
921              
922             } #_create_session
923              
924             sub _destroy_session {
925 0     0   0 my( $self, $token ) = @_;
926            
927 0         0 $self->{STORE}->lock( 'token_mutex' );
928 0         0 my $slots = $self->get__token_timeslots();
929 0         0 for( my $i=0; $i<@$slots; $i++ ) {
930 0         0 delete $slots->[$i]{ $token };
931             }
932 0         0 $self->{STORE}->_stow( $slots );
933 0         0 $self->{STORE}->unlock( 'token_mutex' );
934 0         0 1;
935             } #_destroy_session
936              
937             #
938             # Needed for when no logins are going to happen
939             #
940             sub create_token {
941 1     1   36 shift->_create_session->get__token;
942             }
943              
944             #
945             # Returns the app and possibly a logged in account
946             #
947             sub fetch_app {
948 0     0   0 my( $self, $app_name ) = @_;
949 0         0 my $apps = $self->get__apps;
950 0         0 my $app = $apps->{$app_name};
951              
952 0 0       0 unless( $app ) {
953 0         0 eval("require $app_name");
954 0 0       0 if( $@ ) {
955             # TODO - have/use a good logging system with clarity and stuff
956             # warnings, errors, etc
957 0         0 return undef;
958             }
959 0         0 $app = $app_name->_new( $self->{STORE} );
960 0         0 $apps->{$app_name} = $app;
961             }
962 0 0       0 my $acct = $self->{SESSION} ? $self->{SESSION}->get_acct : undef;
963              
964 0         0 return $app, $acct, $self->{SESSION};
965             } #fetch_app
966              
967             sub fetch_root {
968 2     2   7 return shift;
969             }
970              
971             sub init_root {
972 0     0     my $self = shift;
973 0   0       my $session = $self->{SESSION} || $self->_create_session;
974 0           $self->{SESSION} = $session;
975 0           $session->set__has_ids2times({});
976 0           my $token = $session->get__token;
977 0           return $self, $token;
978             }
979              
980             # while this is a non-op, it will cause any updated contents to be
981             # transfered to the caller automatically
982       1     sub update {
983              
984             }
985              
986             # ------- END Yote::ServerRoot
987              
988             package Yote::ServerSession;
989              
990 12     12   48 use base 'Yote::ServerObj';
  12         12  
  12         3408  
991              
992             sub fetch { # fetch scrambled id
993 0     0     my( $self, $in_sess_id ) = @_;
994 0           $self->get__ids([])->[$in_sess_id-1];
995             }
996              
997             sub getid { #scramble id for object
998 0     0     my( $self, $obj ) = @_;
999 0           my $o2i = $self->get__obj2id({});
1000 0 0         if( $o2i->{$obj} ) {
1001 0           return $o2i->{$obj};
1002             }
1003 0           my $ids = $self->get__ids([]);
1004 0           push @$ids, $obj;
1005 0           my $id = scalar @$ids;
1006 0           $o2i->{$obj} = $id;
1007 0           $id;
1008             } #id
1009              
1010             # ------- END Yote::ServerSession
1011              
1012             1;
1013              
1014             __END__