File Coverage

blib/lib/Yote/Server.pm
Criterion Covered Total %
statement 366 559 65.4
branch 85 188 45.2
condition 36 95 37.8
subroutine 58 82 70.7
pod 1 8 12.5
total 546 932 58.5


line stmt bran cond sub pod time code
1             package Yote::Server;
2              
3 12     12   415020 use strict;
  12         24  
  12         276  
4 12     12   48 use warnings;
  12         24  
  12         276  
5              
6 12     12   48 no warnings 'uninitialized';
  12         60  
  12         420  
7 12     12   48 no warnings 'numeric';
  12         24  
  12         228  
8              
9 12     12   4680 use Lock::Server;
  12         338028  
  12         396  
10 12     12   7620 use Yote;
  12         180444  
  12         360  
11              
12 12     12   120 use bytes;
  12         24  
  12         60  
13 12     12   8076 use IO::Socket::SSL;
  12         511188  
  12         108  
14 12     12   2040 use JSON;
  12         36  
  12         132  
15 12     12   1680 use Time::HiRes qw(time);
  12         36  
  12         132  
16 12     12   6360 use URI::Escape;
  12         15408  
  12         648  
17 12     12   5256 use UUID::Tiny;
  12         99756  
  12         1068  
18              
19              
20 12     12   96 use vars qw($VERSION);
  12         36  
  12         30180  
21              
22             $VERSION = '1.27';
23              
24             our $DEBUG = 0;
25              
26             sub new {
27 12     12 0 7812 my( $pkg, $args ) = @_;
28 12   33     96 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     324 STORE => Yote::ServerStore->_new( { root => $args->{yote_root_dir} } ),
      50        
      50        
      50        
44             }, $class;
45 12         84 $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 30384 my $self = shift;
117              
118 12         132 $self->{_locker}->start;
119              
120 11         19404 my $listener_socket = $self->_create_listener_socket;
121 11 50       110 die "Unable to open socket " unless $listener_socket;
122              
123 11 100       7305 if( my $pid = fork ) {
124             # parent
125 1         40 $self->{server_pid} = $pid;
126 1         44 return $pid;
127             }
128              
129             # in child
130 10         980 $0 = "YoteServer process";
131 10         530 $self->_run_loop( $listener_socket );
132              
133             } #start
134              
135             sub stop {
136 1     1 0 90721 my $self = shift;
137 1 50       18 if( my $pid = $self->{server_pid} ) {
138 1         14 $self->{error} = "Sending INT signal to lock server of pid '$pid'";
139 1         31 kill 'INT', $pid;
140 1         7 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   132 my $self = shift;
162              
163 11         55 my $listener_socket;
164 11         99 my $count = 0;
165              
166 11 50 0     231 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     319 while( ! $listener_socket && ++$count < 10 ) {
170 11 50       242 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         913 $listener_socket = new IO::Socket::INET(
187             Listen => 10,
188             LocalAddr => "$self->{yote_host}:$self->{yote_port}",
189             );
190             }
191 11 50       5654 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       121 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         583 print STDERR "Starting yote server\n";
205              
206 11 50       66 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   34319 _log( "got INT signal. Shutting down." );
215 1 50       82 $listener_socket && $listener_socket->close;
216 1         111 for my $pid ( @{ $self->{_pids} } ) {
  1         13  
217 12         110 kill 'HUP', $pid;
218             }
219 1         36 $self->{_locker}->stop;
220 1         247 exit;
221 11         418 };
222              
223 11         132 $SIG{CHLD} = 'IGNORE';
224              
225 11         143 return $listener_socket;
226             } #_create_listener_socket
227              
228             sub _run_loop {
229 10     10   120 my( $self, $listener_socket ) = @_;
230 10         1050 while( my $connection = $listener_socket->accept ) {
231 57         12769975 $self->_process_request( $connection );
232             }
233             }
234              
235             sub _log {
236 1     1   21 my( $msg, $sev ) = @_;
237 1   50     60 $sev //= 1;
238 1 50       21 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   26 my $data = shift;
248 6         28 my $r = ref( $data );
249 6 50       35 if( $r eq 'ARRAY' ) {
    0          
    0          
250 6 50       25 return grep { $_ && index($_,'v')!=0 } map { ref( $_ ) ? _find_ids_in_data($_) : $_ } @$data;
  7 50       113  
  7         41  
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   168 my( $store, $ids, $seen ) = @_;
267 11   100     105 $seen //= {};
268              
269 11         38 my( @items ) = ( map { $store->fetch($_) } @$ids );
  20         2783  
270              
271 11         1250 my @outids;
272 11         78 for my $item( @items ) {
273 20         145 my $iid = $store->_get_id($item);
274 20         500 my $r = ref( $item );
275 20         59 $seen->{$iid}++;
276 20 100       78 if( $r eq 'ARRAY' ) {
    100          
277 7         62 push @outids, grep { ! $seen->{$_}++ } map { $store->_get_id($_) } grep { ref($_) } @$item;
  7         185  
  7         34  
  21         226  
278             }
279             elsif( $r eq 'HASH' ) {
280 3         51 push @outids, grep { ! $seen->{$_}++ } map { $store->_get_id($_) } grep { ref($_) } values %$item;
  3         123  
  3         1222  
  6         243  
281             }
282             else {
283 10         34 my $data = $item->{DATA};
284 10 100 100     56 push @outids, map { $data->{$_} } grep { /^[^_]/ && $data->{$_} != /^v/ && ! $seen->{$data->{$_}}++ } keys %$data;
  9         52  
  30         429  
285             }
286             }
287              
288 11 100       138 _unroll_ids( $store, \@outids, $seen ) if @outids;
289              
290              
291 11         133 [ 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   292 my( $self, $sock ) = @_;
300              
301              
302 57 100       97345 if ( my $pid = fork ) {
303             # parent
304 48         669 push @{$self->{_pids}},$pid;
  48         7459  
305             } else {
306             # use Devel::SimpleProfiler;Devel::SimpleProfiler::start;
307 9         532 my( $self, $sock ) = @_;
308             #child
309 9         602 $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         952 };
315            
316            
317 9         5322 my $req = <$sock>;
318 9         457 $ENV{REMOTE_HOST} = $sock->peerhost;
319 9         2583 my( %headers, %cookies );
320 9         224 while( my $hdr = <$sock> ) {
321 27         673 $hdr =~ s/\s*$//s;
322 27 100       407 last if $hdr !~ /[a-zA-Z]/;
323 18         245 my( $key, $val ) = ( $hdr =~ /^([^:]+):(.*)/ );
324 18         7360 $headers{$key} = $val;
325             }
326              
327 9         138 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         32 my $content_length = $headers{'Content-Length'};
337 9         67 my $data;
338 9 50 33     551 if ( $content_length > 0 && ! eof $sock) {
339 9         68 read $sock, $data, $content_length;
340             }
341 9         209 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       120 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         105 $data =~ s/^p=//;
384 9         24 my $out_json;
385 9         135 eval {
386 9         135 $out_json = $self->invoke_payload( $data );
387             };
388              
389 9 50       382 if( ref $@ eq 'HASH' ) {
    100          
390 0         0 $out_json = encode_json( $@ );
391             }
392             elsif( $@ ) {
393 3         88 $out_json = encode_json( {
394             err => $@,
395             } );
396             }
397 9         275 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         12665 $sock->print( "HTTP/1.1 200 OK\n" . join ("\n", @headers). "\n\n$out_json\n" );
406            
407 9         1575 $sock->close;
408              
409 9         12819 exit;
410              
411             } #child
412             } #_process_request
413             sub invoke_payload {
414 9     9 0 109 my( $self, $raw_req_data, $file_uploads ) = @_;
415              
416 9         1092 my $req_data = decode_json( $raw_req_data );
417            
418 9         134 my( $obj_id, $token, $action, $params ) = @$req_data{ 'i', 't', 'a', 'pl' };
419            
420 9         370 my $server_root = $self->{STORE}->fetch_server_root;
421 9         319 my $server_root_id = $server_root->{ID};
422            
423              
424 9         29 my $id_to_last_update_time;
425 9 100 66     285 my $session = $token && $token ne '_' ? $server_root->_fetch_session( $token ) : undef;
426              
427 9 100       52 if( $session ) {
428 4         321 $id_to_last_update_time = $session->get__has_ids2times;
429             }
430              
431 9 0 66     1884 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     193 if( substr( $action, 0, 1 ) eq '_' || $action =~ /^[gs]et$/ ) {
438 1         24 die( "Private method called" );
439             }
440              
441 8 50 33     135 if ( $params && ref( $params ) ne 'ARRAY' ) {
442 0         0 die( "Bad Req Param Not Array : $params" );
443             }
444              
445 8         161 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         102 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       194 my $obj = $obj_id eq '_' ? $server_root :
457             $store->fetch( $obj_id );
458              
459 8 100       6412 unless( $obj->can( $action ) ) {
460 2         47 die( "Bad Req : invalid method :'$action'" );
461             }
462              
463             # if there is a session, attach it to the object
464 6 100       35 if( $session ) {
465 2         31 $obj->{SESSION} = $session;
466 2         10 $obj->{SESSION}{SERVER_ROOT} = $server_root;
467              
468             }
469              
470             #
471             # <<------------- the actual method call --------------->>
472             #
473 6         85 my(@res) = ($obj->$action( @$in_params ));
474              
475             #
476             # this is included in what is returned to the client
477             #
478 6         132 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         106 $session = $obj->{SESSION};
484 6 100       31 if( $session ) {
485 2         28 $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         138 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         25 my @should_have = ( @{ _unroll_ids( $store, [@out_ids, keys %$id_to_last_update_time] ) } );
  6         65  
498 6         63 my( @updates, %methods );
499              
500             #
501             # check if existing are in the session
502             #
503 6         58 for my $should_have_id ( @should_have, keys %$id_to_last_update_time ) {
504 24         1001 my $needs_update = 1;
505            
506 24 100       118 if( $session) {
507             #
508             # check if the client of this session needs an update, otherwise assume that it does
509             #
510 18 100       194 my( $client_s, $client_ms ) = @{ $id_to_last_update_time->{$should_have_id} || [] };
  18         120  
511 18         251 my( $server_s, $server_ms ) = $store->_last_updated( $should_have_id );
512              
513 18   66     197 $needs_update = $client_s == 0 || $server_s > $client_s || ($server_s == $client_s && $server_ms > $client_ms );
514             }
515              
516 24 100       106 if( $needs_update ) {
517 12         77 my $should_have_obj = $store->fetch( $should_have_id );
518 12         2404 my $ref = ref( $should_have_obj );
519 12         27 my $data;
520 12 100       54 if( $ref eq 'ARRAY' ) {
    100          
521 4         23 $data = [ map { $store->_xform_in( $_ ) } @$should_have_obj ];
  12         1252  
522             } elsif( $ref eq 'HASH' ) {
523 2         13 $data = { map { $_ => $store->_xform_in( $should_have_obj->{$_} ) } keys %$should_have_obj };
  4         134  
524             } else {
525 6         18 my $d = $should_have_obj->{DATA};
526 10         86 $data = { map { $_ => $d->{$_} } grep { index($_,"_") != 0 } keys %$d },
  20         66  
527 6   66     52 $methods{$ref} ||= $should_have_obj->_callable_methods;
528             }
529 12         192 my $update = {
530             id => $should_have_id,
531             cls => $ref,
532             data => $data,
533             };
534 12         100 push @updates, $update;
535 12 100       59 if( $session ) {
536 6         133 $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         396 my $out_json = to_json( { result => $out_res,
544             updates => \@updates,
545             methods => \%methods,
546             } );
547              
548 6         893 delete $obj->{SESSION};
549 6         70 $self->{STORE}->stow_all;
550            
551 6         8422 return $out_json;
552             } #invoke_payload
553              
554             # ------- END Yote::Server
555              
556             package Yote::ServerStore;
557              
558 12     12   180 use Data::RecordStore;
  12         24  
  12         600  
559              
560 12     12   120 use base 'Yote::ObjStore';
  12         36  
  12         13944  
561              
562             sub _new { #Yote::ServerStore
563 12     12   396 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         9048 $self->{OBJ_UPDATE_DB} = Data::RecordStore::FixedStore->open( "LL", "$args->{root}/OBJ_META" );
571              
572 12         1848 my( $m, $ms ) = ( Time::HiRes::gettimeofday );
573 12         120 $self->{OBJ_UPDATE_DB}->put_record( $self->{ID}, [ $m, $ms ] );
574              
575 12         996 $self;
576             } #_new
577              
578             sub _dirty {
579 398     398   54467 my( $self, $ref, $id ) = @_;
580 398         1465 $self->SUPER::_dirty( $ref, $id );
581 398         2483 $self->{OBJ_UPDATE_DB}->ensure_entry_count( $id );
582              
583 398         30008 my( $m, $ms ) = ( Time::HiRes::gettimeofday );
584 398         1668 $self->{OBJ_UPDATE_DB}->put_record( $id, [ $m, $ms ] );
585             }
586              
587             sub stow_all {
588 31     31   2000460 my $self = $_[0];
589 31         87 for my $obj (values %{$self->{_DIRTY}} ) {
  31         183  
590 167         6411 my $obj_id = $self->_get_id( $obj );
591 167         2762 $self->{OBJ_UPDATE_DB}->ensure_entry_count( $obj_id );
592             }
593 31         1486 $self->SUPER::stow_all;
594             } #stow_all
595              
596             sub _last_updated {
597 18     18   171 my( $self, $obj_id ) = @_;
598 18         41 my( $s, $ms ) = @{ $self->{OBJ_UPDATE_DB}->get_record( $obj_id ) };
  18         88  
599 18         1542 $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   65 my( $self, $param, $session, $files ) = @_;
615              
616 8 50       75 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         83 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 266     266   1066125 my( $self, $val, $files ) = @_;
636 266 100       891 return undef unless defined( $val );
637 247 50       904 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 247         956 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   167782 my( $self, $val, $allow_datastructures ) = @_;
677              
678 276         2328 my $r = ref $val;
679 276 100       729 if( $r ) {
680 179 100       435 if( $allow_datastructures) {
681             # check if this is a yote object
682 8 100 66     131 if( ref( $val ) eq 'ARRAY' && ! tied( @$val ) ) {
    50 33        
683 6 100       36 return [ map { ref $_ ? $self->_xform_in( $_, $allow_datastructures ) : "v$_" } @$val ];
  7         66  
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         554 return $self->_get_id( $val );
690             }
691              
692 97 50       598 return defined $val ? "v$val" : undef;
693             } #_xform_in
694              
695             sub newobj {
696 25     25   424 my( $self, $data, $class ) = @_;
697 25   100     190 $class ||= 'Yote::ServerObj';
698 25         272 $class->_new( $self, $data );
699             } #newobj
700              
701             sub fetch_server_root {
702 21     21   1996 my $self = shift;
703              
704 21 100       1374 return $self->{SERVER_ROOT} if $self->{SERVER_ROOT};
705              
706 12         132 my $system_root = $self->fetch_root;
707 12         8832 my $server_root = $system_root->get_server_root;
708 12 50       48 unless( $server_root ) {
709 12         108 $server_root = Yote::ServerRoot->_new( $self );
710 12         324 $system_root->set_server_root( $server_root );
711 12         216 $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     45420 $self->{SERVER_ROOT} ||= $server_root;
722              
723 12         1092 $server_root;
724            
725             } #fetch_server_root
726              
727             sub lock {
728 5     5   38 my( $self, $key ) = @_;
729 5   33     290 $self->{_lockerClient} ||= $self->{_locker}->client( $$ );
730 5         830 $self->{_lockerClient}->lock( $key );
731             }
732              
733             sub unlock {
734 5     5   50 my( $self, $key ) = @_;
735 5         43 $self->{_lockerClient}->unlock( $key );
736             }
737              
738              
739             # ------- END Yote::ServerStore
740              
741             package Yote::ServerObj;
742              
743 12     12   84 use base 'Yote::Obj';
  12         24  
  12         4644  
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   18 my $pkg = shift;
757 6         18 my $meths = $Yote::ServerObj::PKG2METHS->{$pkg};
758 6 50       27 if( $meths ) {
759 0         0 return $meths;
760             }
761              
762 12     12   84 no strict 'refs';
  12         36  
  12         5328  
763 6         14 my @m = grep { $_ !~ /::/ } keys %{"${pkg}\::"};
  131         451  
  6         232  
764 6 100       42 if( $pkg eq 'Yote::ServerObj' ) { #the base, presumably
765 4         15 return [ sort grep { $_ !~ /^(_|[gs]et_|(can|[sg]et|VERSION|AUTOLOAD|DESTROY|CARP_TRACE|BEGIN|isa|import|PKG2METHS|ISA|add_to_|remove_from)$)/ } @m ];
  68         297  
766             }
767              
768 2         9 my %hasm = map { $_ => 1 } @m;
  63         185  
769 2         12 for my $class ( @{"${pkg}\::ISA" } ) {
  2         17  
770 2 50 33     18 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         16 my $base_meths = __discover_methods( 'Yote::ServerObj' );
776 2         11 my( %base ) = map { $_ => 1 } 'AUTOLOAD', @$base_meths;
  6         23  
777 2   66     9 $meths = [ sort grep { $_ !~ /^(_|[gs]et_|(can|[sg]et|VERSION|AUTOLOAD|DESTROY|BEGIN|isa|import|PKG2METHS|ISA|add_to_|remove_from)$)/ && ! $base{$_} } @m ];
  63         348  
778 2         12 $Yote::ServerObj::PKG2METHS->{$pkg} = $meths;
779            
780 2         21 $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   15 my $self = shift;
788 4         14 my $pkg = ref( $self );
789 4         18 __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   84 use base 'Yote::ServerObj';
  12         12  
  12         9984  
811              
812             sub _init {
813 12     12   888 my $self = shift;
814 12         180 $self->set__doesHave_Token2objs({});
815 12         312 $self->set__apps({});
816 12         276 $self->set__token_timeslots([]);
817 12         312 $self->set__token_timeslots_metadata([]);
818 12         300 $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   24 my( $self, $token ) = @_;
837            
838 4         56 $self->{STORE}->lock( 'token_mutex' );
839 4         7984 my $slots = $self->get__token_timeslots();
840              
841 4         2705 for( my $i=0; $i<@$slots; $i++ ) {
842 4 50       87 if( my $session = $slots->[$i]{$token} ) {
843 4 50       4207 if( $i > 0 ) {
844             # make sure this is in the most current 'boat'
845 0         0 $slots->[0]{ $token } = $session;
846             }
847 4         37 $self->{STORE}->unlock( 'token_mutex' );
848 4         6272 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         3 my $tries = shift;
858              
859 1 50       10 if( $tries > 3 ) {
860 0         0 die "Error creating token. Got the same random number 4 times in a row";
861             }
862              
863 1         11 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         6 my $current_time_chunk = int( time / 100 );
870 1         4 my $earliest_valid_time_chunk = $current_time_chunk - 7;
871              
872 1         8 $self->{STORE}->lock( 'token_mutex' );
873              
874             #
875             # A list of slot 'boats' which store token -> ip
876             #
877 1         2386 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         394 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         293 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         29 _has_ids2times => {},
898             _token => $token }, 'Yote::ServerSession' );
899            
900 1 50       138 if( $slot_data->[ 0 ] == $current_time_chunk ) {
901 0         0 $slots->[ 0 ]{ $token } = $session;
902             } else {
903 1         16 unshift @$slot_data, $current_time_chunk;
904 1         8 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         158 for( my $i=1; $i<@$slot_data; $i++ ) {
912 0         0 delete $slots->[$i]{ $token };
913             }
914              
915 1         24 $self->{STORE}->_stow( $slots );
916 1         445 $self->{STORE}->_stow( $slot_data );
917 1         1099 $self->{STORE}->unlock( 'token_mutex' );
918              
919              
920 1         1650 $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   11 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   16 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   96 use base 'Yote::ServerObj';
  12         36  
  12         4176  
991              
992             sub fetch { # fetch scrambled id
993 0     0     my( $self, $in_sess_id ) = @_;
994 0 0         return unless $in_sess_id > 0;
995 0           $self->get__ids([])->[$in_sess_id-1];
996             }
997              
998             sub getid { #scramble id for object
999 0     0     my( $self, $obj ) = @_;
1000 0           my $o2i = $self->get__obj2id({});
1001 0 0         if( $o2i->{$obj} ) {
1002 0           return $o2i->{$obj};
1003             }
1004 0           my $ids = $self->get__ids([]);
1005 0           push @$ids, $obj;
1006 0           my $id = scalar @$ids;
1007 0           $o2i->{$obj} = $id;
1008 0           $id;
1009             } #id
1010              
1011             # ------- END Yote::ServerSession
1012              
1013             package Yote::Server::Acct;
1014              
1015 12     12   96 use strict;
  12         24  
  12         276  
1016 12     12   48 use warnings;
  12         24  
  12         312  
1017              
1018 12     12   132 use Yote::Server;
  12         24  
  12         396  
1019              
1020 12     12   60 use base 'Yote::ServerObj';
  12         24  
  12         3108  
1021              
1022       0     sub _onLogin {}
1023              
1024             sub logout {
1025 0     0     my $self = shift;
1026 0           my $server = $self->{SESSION}{SERVER};
1027 0           $server->_destroy_session( $self->{SESSION}->get__token );
1028             } #logout
1029              
1030             # ------- END Yote::Server::Acct
1031              
1032             package Yote::Server::App;
1033              
1034 12     12   120 use strict;
  12         36  
  12         408  
1035 12     12   108 use warnings;
  12         24  
  12         336  
1036              
1037 12     12   60 use Yote::Server;
  12         24  
  12         240  
1038              
1039 12     12   72 use Digest::MD5;
  12         24  
  12         528  
1040              
1041 12     12   60 use base 'Yote::ServerObj';
  12         12  
  12         5748  
1042              
1043 0     0     sub _acct_class { "Yote::Server::Acct" }
1044              
1045             #
1046             # Override and call _create_account
1047             #
1048             sub create_account {
1049 0     0     die "May not create account via website";
1050             }
1051              
1052             sub _create_account {
1053 0     0     my( $self, $un, $pw, $class_override ) = @_;
1054 0           my $accts = $self->get__accts({});
1055              
1056 0 0         if( $accts->{lc($un)} ) {
1057 0           $self->_err( "Unable to create account" );
1058             }
1059              
1060 0   0       my $acct = $self->{STORE}->newobj( { user => $un }, $class_override || $self->_acct_class );
1061 0           $acct->set__password_hash( crypt( $pw, length( $pw ) . Digest::MD5::md5_hex($acct->{ID} ) ) );
1062              
1063             # TODO - create an email infrastructure for account validation
1064 0           $acct->set_app( $self );
1065            
1066 0           $accts->{lc($un)} = $acct;
1067 0           $acct;
1068             } #_create_account
1069              
1070             sub logout {
1071 0     0     my $self = shift;
1072 0           my $root = $self->{SESSION}{SERVER_ROOT};
1073 0 0         $root->_destroy_session( $self->{SESSION}->get__token ) if $root;
1074 0           delete $self->{SESSION};
1075 0           1;
1076             } #logout
1077              
1078             sub login {
1079 0     0     my( $self, $un, $pw ) = @_;
1080              
1081             # returns account, cookie. only way to get account object
1082 0           my $acct = $self->get__accts({})->{lc($un)};
1083              
1084             # doing it like this so a failed attempt has about the same amount of time
1085             # as an attempt against a nonexistant account. maybe random microsleep?
1086 0 0         my $pwh = crypt( $pw, length( $pw ) . Digest::MD5::md5_hex($acct ? $acct->{ID} : $self->{ID} ) );
1087 0 0 0       if( $acct && $pwh eq $acct->get__password_hash ) {
1088             # this and Yote::ServerRoot::fetch_app are the only ways to expose the account obj
1089             # to the UI. If the UI calls for an acct object it wasn't exposed to, Yote::Server
1090             # won't allow it. fetch_app only calls it if the correct cookie token is passed in
1091 0           $self->{SESSION}->set_acct( $acct );
1092 0           $acct->_onLogin;
1093 0           return $acct;
1094             }
1095 0           $self->_err( "Incorrect login" );
1096             } #login
1097              
1098             # ------- END Yote::Server::Acct
1099              
1100             1;
1101              
1102             __END__