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   495744 use strict;
  12         12  
  12         264  
4 12     12   48 use warnings;
  12         12  
  12         240  
5              
6 12     12   36 no warnings 'uninitialized';
  12         60  
  12         384  
7 12     12   48 no warnings 'numeric';
  12         0  
  12         240  
8              
9 12     12   6312 use Lock::Server;
  12         289524  
  12         360  
10 12     12   6840 use Yote;
  12         174204  
  12         348  
11              
12 12     12   72 use bytes;
  12         12  
  12         48  
13 12     12   8976 use IO::Socket::SSL;
  12         485340  
  12         108  
14 12     12   1932 use JSON;
  12         12  
  12         72  
15 12     12   1020 use Time::HiRes qw(time);
  12         12  
  12         108  
16 12     12   5616 use URI::Escape;
  12         11820  
  12         864  
17 12     12   5664 use UUID::Tiny;
  12         97104  
  12         1092  
18              
19              
20 12     12   72 use vars qw($VERSION);
  12         12  
  12         29040  
21              
22             $VERSION = '1.25';
23              
24             our $DEBUG = 0;
25              
26             sub new {
27 12     12 0 7236 my( $pkg, $args ) = @_;
28 12   33     84 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     264 STORE => Yote::ServerStore->_new( { root => $args->{yote_root_dir} } ),
      50        
      50        
      50        
44             }, $class;
45 12         72 $server->{STORE}{_locker} = $server->{_locker};
46 12         36 $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 20892 my $self = shift;
117              
118 12         84 $self->{_locker}->start;
119              
120 11         17182 my $listener_socket = $self->_create_listener_socket;
121 11 50       110 die "Unable to open socket " unless $listener_socket;
122              
123 11 100       7265 if( my $pid = fork ) {
124             # parent
125 1         23 $self->{server_pid} = $pid;
126 1         28 return $pid;
127             }
128              
129             # in child
130 10         750 $0 = "YoteServer process";
131 10         2680 $self->_run_loop( $listener_socket );
132              
133             } #start
134              
135             sub stop {
136 1     1 0 77001 my $self = shift;
137 1 50       6 if( my $pid = $self->{server_pid} ) {
138 1         13 $self->{error} = "Sending INT signal to lock server of pid '$pid'";
139 1         29 kill 'INT', $pid;
140 1         5 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   110 my $self = shift;
162              
163 11         55 my $listener_socket;
164 11         77 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     297 while( ! $listener_socket && ++$count < 10 ) {
170 11 50       110 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         462 $listener_socket = new IO::Socket::INET(
187             Listen => 10,
188             LocalAddr => "$self->{yote_host}:$self->{yote_port}",
189             );
190             }
191 11 50       4147 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       77 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         330 print STDERR "Starting yote server\n";
205              
206 11 50       88 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   26093 _log( "got INT signal. Shutting down." );
215 1 50       35 $listener_socket && $listener_socket->close;
216 1         98 for my $pid ( @{ $self->{_pids} } ) {
  1         9  
217 12         70 kill 'HUP', $pid;
218             }
219 1         33 $self->{_locker}->stop;
220 1         3962 exit;
221 11         253 };
222              
223 11         143 $SIG{CHLD} = 'IGNORE';
224              
225 11         88 return $listener_socket;
226             } #_create_listener_socket
227              
228             sub _run_loop {
229 10     10   100 my( $self, $listener_socket ) = @_;
230 10         1010 while( my $connection = $listener_socket->accept ) {
231 57         12545722 $self->_process_request( $connection );
232             }
233             }
234              
235             sub _log {
236 1     1   5 my( $msg, $sev ) = @_;
237 1   50     44 $sev //= 1;
238 1 50       13 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   13 my $data = shift;
248 6         15 my $r = ref( $data );
249 6 50       22 if( $r eq 'ARRAY' ) {
    0          
    0          
250 6 50       17 return grep { $_ && index($_,'v')!=0 } map { ref( $_ ) ? _find_ids_in_data($_) : $_ } @$data;
  7 50       74  
  7         25  
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   92 my( $store, $ids, $seen ) = @_;
267 11   100     75 $seen //= {};
268              
269 11         13 my( @items ) = ( map { $store->fetch($_) } @$ids );
  22         3266  
270              
271 11         1211 my @outids;
272 11         42 for my $item( @items ) {
273 22         68 my $iid = $store->_get_id($item);
274 22         256 my $r = ref( $item );
275 22         34 $seen->{$iid}++;
276 22 100       53 if( $r eq 'ARRAY' ) {
    100          
277 8         44 push @outids, grep { ! $seen->{$_}++ } map { $store->_get_id($_) } grep { ref($_) } @$item;
  8         136  
  8         22  
  24         104  
278             }
279             elsif( $r eq 'HASH' ) {
280 4         24 push @outids, grep { ! $seen->{$_}++ } map { $store->_get_id($_) } grep { ref($_) } values %$item;
  4         72  
  4         346  
  8         618  
281             }
282             else {
283 10         14 my $data = $item->{DATA};
284 10 100 100     33 push @outids, map { $data->{$_} } grep { /^[^_]/ && $data->{$_} != /^v/ && ! $seen->{$data->{$_}}++ } keys %$data;
  11         33  
  30         247  
285             }
286             }
287              
288 11 100       71 _unroll_ids( $store, \@outids, $seen ) if @outids;
289              
290              
291 11         77 [ 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   136 my( $self, $sock ) = @_;
300              
301              
302 57 100       78436 if ( my $pid = fork ) {
303             # parent
304 48         295 push @{$self->{_pids}},$pid;
  48         30324  
305             } else {
306             # use Devel::SimpleProfiler;Devel::SimpleProfiler::start;
307 9         422 my( $self, $sock ) = @_;
308             #child
309 9         535 $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         821 };
315            
316            
317 9         1256 my $req = <$sock>;
318 9         472 $ENV{REMOTE_HOST} = $sock->peerhost;
319 9         2125 my( %headers, %cookies );
320 9         354 while( my $hdr = <$sock> ) {
321 27         662 $hdr =~ s/\s*$//s;
322 27 100       327 last if $hdr !~ /[a-zA-Z]/;
323 18         203 my( $key, $val ) = ( $hdr =~ /^([^:]+):(.*)/ );
324 18         214 $headers{$key} = $val;
325             }
326              
327 9         119 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         77 my $content_length = $headers{'Content-Length'};
337 9         56 my $data;
338 9 50 33     411 if ( $content_length > 0 && ! eof $sock) {
339 9         86 read $sock, $data, $content_length;
340             }
341 9         263 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       70 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       97 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         58 $data =~ s/^p=//;
384 9         53 my $out_json;
385 9         96 eval {
386 9         220 $out_json = $self->invoke_payload( $data );
387             };
388              
389 9 50       156 if( ref $@ eq 'HASH' ) {
    100          
390 0         0 $out_json = encode_json( $@ );
391             }
392             elsif( $@ ) {
393 3         61 $out_json = encode_json( {
394             err => $@,
395             } );
396             }
397 9         265 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         11919 $sock->print( "HTTP/1.1 200 OK\n" . join ("\n", @headers). "\n\n$out_json\n" );
406            
407 9         1500 $sock->close;
408              
409 9         10990 exit;
410              
411             } #child
412             } #_process_request
413             sub invoke_payload {
414 9     9 0 16 my( $self, $raw_req_data, $file_uploads ) = @_;
415              
416 9         420 my $req_data = decode_json( $raw_req_data );
417            
418 9         79 my( $obj_id, $token, $action, $params ) = @$req_data{ 'i', 't', 'a', 'pl' };
419            
420 9         232 my $server_root = $self->{STORE}->fetch_server_root;
421 9         172 my $server_root_id = $server_root->{ID};
422            
423              
424 9         13 my $id_to_last_update_time;
425 9 100 66     272 my $session = $token && $token ne '_' ? $server_root->_fetch_session( $token ) : undef;
426              
427 9 100       55 if( $session ) {
428 4         101 $id_to_last_update_time = $session->get__has_ids2times;
429             }
430              
431 9 0 66     1201 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     150 if( substr( $action, 0, 1 ) eq '_' || $action =~ /^[gs]et$/ ) {
438 1         23 die( "Private method called" );
439             }
440              
441 8 50 33     74 if ( $params && ref( $params ) ne 'ARRAY' ) {
442 0         0 die( "Bad Req Param Not Array : $params" );
443             }
444              
445 8         27 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         82 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       105 my $obj = $obj_id eq '_' ? $server_root :
457             $store->fetch( $obj_id );
458              
459 8 100       5051 unless( $obj->can( $action ) ) {
460 2         52 die( "Bad Req : invalid method :'$action'" );
461             }
462              
463             # if there is a session, attach it to the object
464 6 100       34 if( $session ) {
465 2         19 $obj->{SESSION} = $session;
466 2         6 $obj->{SESSION}{SERVER_ROOT} = $server_root;
467              
468             }
469              
470             #
471             # <<------------- the actual method call --------------->>
472             #
473 6         294 my(@res) = ($obj->$action( @$in_params ));
474              
475             #
476             # this is included in what is returned to the client
477             #
478 6         80 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       20 if( $session ) {
485 2         18 $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         52 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         8 my @should_have = ( @{ _unroll_ids( $store, [@out_ids, keys %$id_to_last_update_time] ) } );
  6         49  
498 6         17 my( @updates, %methods );
499              
500             #
501             # check if existing are in the session
502             #
503 6         41 for my $should_have_id ( @should_have, keys %$id_to_last_update_time ) {
504 24         699 my $needs_update = 1;
505            
506 24 100       69 if( $session) {
507             #
508             # check if the client of this session needs an update, otherwise assume that it does
509             #
510 18 100       110 my( $client_s, $client_ms ) = @{ $id_to_last_update_time->{$should_have_id} || [] };
  18         87  
511 18         139 my( $server_s, $server_ms ) = $store->_last_updated( $should_have_id );
512              
513 18   66     145 $needs_update = $client_s == 0 || $server_s > $client_s || ($server_s == $client_s && $server_ms > $client_ms );
514             }
515              
516 24 100       65 if( $needs_update ) {
517 12         49 my $should_have_obj = $store->fetch( $should_have_id );
518 12         1780 my $ref = ref( $should_have_obj );
519 12         13 my $data;
520 12 100       39 if( $ref eq 'ARRAY' ) {
    100          
521 4         19 $data = [ map { $store->_xform_in( $_ ) } @$should_have_obj ];
  12         873  
522             } elsif( $ref eq 'HASH' ) {
523 2         9 $data = { map { $_ => $store->_xform_in( $should_have_obj->{$_} ) } keys %$should_have_obj };
  4         70  
524             } else {
525 6         11 my $d = $should_have_obj->{DATA};
526 10         63 $data = { map { $_ => $d->{$_} } grep { index($_,"_") != 0 } keys %$d },
  20         33  
527 6   66     39 $methods{$ref} ||= $should_have_obj->_callable_methods;
528             }
529 12         110 my $update = {
530             id => $should_have_id,
531             cls => $ref,
532             data => $data,
533             };
534 12         20 push @updates, $update;
535 12 100       39 if( $session ) {
536 6         87 $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         238 my $out_json = to_json( { result => $out_res,
544             updates => \@updates,
545             methods => \%methods,
546             } );
547              
548 6         229 delete $obj->{SESSION};
549 6         56 $self->{STORE}->stow_all;
550            
551 6         7734 return $out_json;
552             } #invoke_payload
553              
554             # ------- END Yote::Server
555              
556             package Yote::ServerStore;
557              
558 12     12   204 use Data::RecordStore;
  12         24  
  12         264  
559              
560 12     12   48 use base 'Yote::ObjStore';
  12         12  
  12         13884  
561              
562             sub _new { #Yote::ServerStore
563 12     12   252 my( $pkg, $args ) = @_;
564 12         48 $args->{store} = "$args->{root}/DATA_STORE";
565 12         108 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         10416 $self->{OBJ_UPDATE_DB} = Data::RecordStore::FixedStore->open( "LL", "$args->{root}/OBJ_META" );
571              
572 12         1944 my( $m, $ms ) = ( Time::HiRes::gettimeofday );
573 12         72 $self->{OBJ_UPDATE_DB}->put_record( $self->{ID}, [ $m, $ms ] );
574              
575 12         948 $self;
576             } #_new
577              
578             sub _dirty {
579 398     398   41296 my( $self, $ref, $id ) = @_;
580 398         782 $self->SUPER::_dirty( $ref, $id );
581 398         1455 $self->{OBJ_UPDATE_DB}->ensure_entry_count( $id );
582              
583 398         31785 my( $m, $ms ) = ( Time::HiRes::gettimeofday );
584 398         1070 $self->{OBJ_UPDATE_DB}->put_record( $id, [ $m, $ms ] );
585             }
586              
587             sub stow_all {
588 31     31   2000412 my $self = $_[0];
589 31         36 for my $obj (values %{$self->{_DIRTY}} ) {
  31         160  
590 167         7829 my $obj_id = $self->_get_id( $obj );
591 167         1699 $self->{OBJ_UPDATE_DB}->ensure_entry_count( $obj_id );
592             }
593 31         1826 $self->SUPER::stow_all;
594             } #stow_all
595              
596             sub _last_updated {
597 18     18   26 my( $self, $obj_id ) = @_;
598 18         18 my( $s, $ms ) = @{ $self->{OBJ_UPDATE_DB}->get_record( $obj_id ) };
  18         61  
599 18         1126 $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       80 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         34 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 271     271   1053836 my( $self, $val, $files ) = @_;
636 271 100       593 return undef unless defined( $val );
637 252 50       701 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 252         616 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   108666 my( $self, $val, $allow_datastructures ) = @_;
677              
678 276         1232 my $r = ref $val;
679 276 100       433 if( $r ) {
680 179 100       318 if( $allow_datastructures) {
681             # check if this is a yote object
682 8 100 66     170 if( ref( $val ) eq 'ARRAY' && ! tied( @$val ) ) {
    50 33        
683 6 100       25 return [ map { ref $_ ? $self->_xform_in( $_, $allow_datastructures ) : "v$_" } @$val ];
  7         63  
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         358 return $self->_get_id( $val );
690             }
691              
692 97 50       415 return defined $val ? "v$val" : undef;
693             } #_xform_in
694              
695             sub newobj {
696 25     25   137 my( $self, $data, $class ) = @_;
697 25   100     140 $class ||= 'Yote::ServerObj';
698 25         237 $class->_new( $self, $data );
699             } #newobj
700              
701             sub fetch_server_root {
702 21     21   981 my $self = shift;
703              
704 21 100       1324 return $self->{SERVER_ROOT} if $self->{SERVER_ROOT};
705              
706 12         72 my $system_root = $self->fetch_root;
707 12         11760 my $server_root = $system_root->get_server_root;
708 12 50       36 unless( $server_root ) {
709 12         96 $server_root = Yote::ServerRoot->_new( $self );
710 12         204 $system_root->set_server_root( $server_root );
711 12         480 $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     44256 $self->{SERVER_ROOT} ||= $server_root;
722              
723 12         108 $server_root;
724            
725             } #fetch_server_root
726              
727             sub lock {
728 5     5   38 my( $self, $key ) = @_;
729 5   33     411 $self->{_lockerClient} ||= $self->{_locker}->client( $$ );
730 5         831 $self->{_lockerClient}->lock( $key );
731             }
732              
733             sub unlock {
734 5     5   12 my( $self, $key ) = @_;
735 5         38 $self->{_lockerClient}->unlock( $key );
736             }
737              
738              
739             # ------- END Yote::ServerStore
740              
741             package Yote::ServerObj;
742              
743 12     12   72 use base 'Yote::Obj';
  12         0  
  12         3672  
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   16 my $pkg = shift;
757 6         9 my $meths = $Yote::ServerObj::PKG2METHS->{$pkg};
758 6 50       16 if( $meths ) {
759 0         0 return $meths;
760             }
761              
762 12     12   48 no strict 'refs';
  12         12  
  12         4884  
763 6         7 my @m = grep { $_ !~ /::/ } keys %{"${pkg}\::"};
  135         202  
  6         148  
764 6 100       28 if( $pkg eq 'Yote::ServerObj' ) { #the base, presumably
765 4         6 return [ sort grep { $_ !~ /^(_|[gs]et_|(can|[sg]et|VERSION|AUTOLOAD|DESTROY|CARP_TRACE|BEGIN|isa|import|PKG2METHS|ISA|add_to_|remove_from)$)/ } @m ];
  72         184  
766             }
767              
768 2         5 my %hasm = map { $_ => 1 } @m;
  63         93  
769 2         7 for my $class ( @{"${pkg}\::ISA" } ) {
  2         13  
770 2 50 33     14 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         21 my $base_meths = __discover_methods( 'Yote::ServerObj' );
776 2         7 my( %base ) = map { $_ => 1 } 'AUTOLOAD', @$base_meths;
  6         39  
777 2   66     7 $meths = [ sort grep { $_ !~ /^(_|[gs]et_|(can|[sg]et|VERSION|AUTOLOAD|DESTROY|BEGIN|isa|import|PKG2METHS|ISA|add_to_|remove_from)$)/ && ! $base{$_} } @m ];
  63         196  
778 2         16 $Yote::ServerObj::PKG2METHS->{$pkg} = $meths;
779            
780 2         16 $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         5 my $pkg = ref( $self );
789 4         14 __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         9888  
811              
812             sub _init {
813 12     12   768 my $self = shift;
814 12         132 $self->set__doesHave_Token2objs({});
815 12         228 $self->set__apps({});
816 12         204 $self->set__token_timeslots([]);
817 12         204 $self->set__token_timeslots_metadata([]);
818 12         216 $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   11 my( $self, $token ) = @_;
837            
838 4         19 $self->{STORE}->lock( 'token_mutex' );
839 4         6522 my $slots = $self->get__token_timeslots();
840              
841 4         2530 for( my $i=0; $i<@$slots; $i++ ) {
842 4 50       57 if( my $session = $slots->[$i]{$token} ) {
843 4 50       3794 if( $i > 0 ) {
844             # make sure this is in the most current 'boat'
845 0         0 $slots->[0]{ $token } = $session;
846             }
847 4         32 $self->{STORE}->unlock( 'token_mutex' );
848 4         5234 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   3 my $self = shift;
857 1         2 my $tries = shift;
858              
859 1 50       3 if( $tries > 3 ) {
860 0         0 die "Error creating token. Got the same random number 4 times in a row";
861             }
862              
863 1         7 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         4 my $current_time_chunk = int( time / 100 );
870 1         3 my $earliest_valid_time_chunk = $current_time_chunk - 7;
871              
872 1         4 $self->{STORE}->lock( 'token_mutex' );
873              
874             #
875             # A list of slot 'boats' which store token -> ip
876             #
877 1         1456 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         232 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         138 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         15 _has_ids2times => {},
898             _token => $token }, 'Yote::ServerSession' );
899            
900 1 50       66 if( $slot_data->[ 0 ] == $current_time_chunk ) {
901 0         0 $slots->[ 0 ]{ $token } = $session;
902             } else {
903 1         18 unshift @$slot_data, $current_time_chunk;
904 1         4 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         94 for( my $i=1; $i<@$slot_data; $i++ ) {
912 0         0 delete $slots->[$i]{ $token };
913             }
914              
915 1         17 $self->{STORE}->_stow( $slots );
916 1         234 $self->{STORE}->_stow( $slot_data );
917 1         154 $self->{STORE}->unlock( 'token_mutex' );
918              
919              
920 1         859 $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   7 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   8 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   60 use base 'Yote::ServerObj';
  12         12  
  12         3516  
991              
992             sub fetch { # fetch scrambled id
993 0     0     my( $self, $in_sess_id ) = @_;
994 0           $self->get__ids([])->[$in_sess_id];
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 = $#$ids;
1006 0           $o2i->{$obj} = $id;
1007 0           $id;
1008             } #id
1009              
1010             # ------- END Yote::ServerSession
1011              
1012             1;
1013              
1014             __END__