File Coverage

blib/lib/Dancer2/Plugin/WebService.pm
Criterion Covered Total %
statement 18 442 4.0
branch 0 212 0.0
condition 0 54 0.0
subroutine 6 26 23.0
pod 7 9 77.7
total 31 743 4.1


line stmt bran cond sub pod time code
1             # Dancer2 plugin for easy create restfull web services
2             # Provides Routes for authentication, persistent session data
3             # It can handle the formats : JSON , XML , YAML, PERL , HUMAN
4             #
5             # Quick diffences between plugin and Dancer2 core
6             #
7             # main plugin
8             # -------------------------------------------------------------
9             # Dancer2 method $plugin->app->DancerMethod
10             # setting('Key') or config->{Key} $plugin->config->{key} , $plugin->app->{name}, $plugin->{app}->{name},
11             #
12             # change setting : $plugin->{app}->{config}->{SomeSetting} = 'foo';
13             #
14             # example of start command
15             #
16             # sudo -u joe /usr/bin/site_perl/plackup --host 0.0.0.0 --port 65535 --server Starman --workers=10 -a /opt/TestService/bin/app.psgi --Reload /opt/TestService/lib,/opt/TestService/config.yml,/usr/share/perl5/site_perl/Dancer2/Plugin
17             #
18             # George Mpouras
19             # george.mpouras@yandex.com
20             # 25 Sep 2016 , Athens - Greece
21              
22              
23              
24              
25             package Dancer2::Plugin::WebService;
26 1     1   45638 use Dancer2::Plugin;
  1         198966  
  1         5  
27 1     1   24940 use Storable;
  1         2337  
  1         2844  
28             our $VERSION = '3.009';
29              
30             if ($^O =~ /(?i)MSWin/) {warn "Sorry windows operating system is not supported\n"; exit 1}
31              
32              
33             # Make available the following functions to applications
34             plugin_keywords qw/
35              
36             get_data_user
37             set_data_user
38             del_data_user
39             get_data_session
40             set_data_session
41             del_data_session
42             RestReply
43             /;
44              
45              
46             # Change rw properties later like $plugin->SomeProperty('new value');
47             has dir_root => (is=>'ro', default=> sub{ ($_=$_[0]->{app}->{config}->{appdir}) =~s/\/*$//; if (-d $_) { $_ } else { warn "Could not define root directory\n"; exit 1 }});
48             has formats => (is=>'ro', default=> sub{{ json => 'application/json', xml => 'text/xml', yaml => 'text/x-yaml', perl => 'text/html', human => 'text/html' }});
49             has formats_regex => (is=>'ro', default=> sub{ $_=join '|', sort keys %{ $_[0]->formats }; $_ = qr/^($_)$/; $_ });
50             has ClientIP => (is=>'rw', default=> '');
51             has route_name => (is=>'rw', default=> '');
52             has error => (is=>'rw', default=> 0);
53             has errormessage => (is=>'rw', default=> 'ok');
54             has data_user => (is=>'rw', default=> sub{ {} }); # hash holding the data user send to us at "data_from"
55             has data_from => (is=>'rw', default=> ''); # string of user data
56             has data_to => (is=>'rw', default=> ''); # string of user data rebuilded
57             has Authentication_method=> (is=>'rw', default=> '');
58             has auth_member_of => (is=>'rw', default=> sub{ [] });
59             has auth_result => (is=>'rw', default=> 0);
60             has auth_message => (is=>'rw', default=> '');
61             has from => (is=>'rw', from_config=>'Default format', default=> sub{ 'json' });
62             has to => (is=>'rw', default=> sub{ $_[0]->from });
63             has sudo => (is=>'ro', from_config=>'Command sudo', default=> sub{ '/usr/bin/sudo' });
64             has rm => (is=>'ro', from_config=>'Command rm', default=> sub{ '/usr/bin/rm' });
65             has groups => (is=>'ro', from_config=>'User must belong to one or more of the groups', default=> sub{ [] });
66             has Session_idle_timout => (is=>'ro', from_config=>'Session idle timout', default=> sub{ 3600 } );
67             has rules => (is=>'ro', from_config=>'Allowed hosts',default=> sub{ ['127.*', '192.168.*', '10.*', '172.16.*'] });
68             has rules_compiled => (is=>'ro', default=> sub {my $array = [@{$_[0]->rules}]; for (@{$array}) { s/([^?*]+)/\Q$1\E/g; s|\?|.|g; s|\*+|.*?|g; $_ = qr/^$_$/i } $array});
69             has dir_session => (is=>'ro', default=> sub {$_ = exists $_[0]->{app}->{config}->{plugins}->{WebService}->{'Session directory'} ? $_[0]->{app}->{config}->{plugins}->{WebService}->{'Session directory'} : $_[0]->dir_root .'/sessions'; $_ .= "/$_[0]->{app}->{config}->{appname}" if $_ !~/$_[0]->{app}->{config}->{appname}$/; $_});
70              
71              
72              
73              
74             sub BUILD
75             {
76 0     0 0   my $plugin = shift;
77              
78             # Define the built in Routes
79 0           $plugin->config->{Routes}->{info} = 'public';
80 0           $plugin->config->{Routes}->{login} = 'public';
81 0           $plugin->config->{Routes}->{logout} = 'private';
82              
83             # Default Dancer2 settings
84 0           $plugin->{app}->{config}->{content_type}= 'application/json';
85 0 0         $plugin->{app}->{config}->{charset} = 'utf-8' if $plugin->{app}->{config}->{charset} eq '';
86 0   0       $plugin->{app}->{config}->{encoding} //= 'UTF-8';
87 0   0       $plugin->{app}->{config}->{show_errors} //= 1;
88 0   0       $plugin->{app}->{config}->{auto_page} //= 0;
89 0   0       $plugin->{app}->{config}->{traces} //= 0;
90 0   0       $plugin->{app}->{config}->{layout} //= 'main';
91 0   0       $plugin->{app}->{config}->{behind_proxy}//= 0;
92 0   0       $plugin->{app}->{config}->{plugins}->{WebService}->{'Default format'} //= 'json';
93 0   0       $plugin->{app}->{config}->{plugins}->{WebService}->{'Command sudo'} //= '/usr/bin/sudo';
94 0   0       $plugin->{app}->{config}->{plugins}->{WebService}->{'Command rm'} //= '/usr/bin/rm';
95 0   0       $plugin->{app}->{config}->{plugins}->{WebService}->{'Owner'} //= 'Joe Lunchbucket';
96 0   0       $plugin->{app}->{config}->{plugins}->{WebService}->{'Session idle timout'}//= 3600;
97              
98 0 0         __MKDIR($plugin->dir_session) or die 'Could not create the session directory '.$plugin->dir_session." because $!\n";
99              
100             # Find the active authentication method
101 0           (my $module_dir =__FILE__) =~s/\/(?>[^\/]+)$//;
102 0 0         unless (-d $module_dir) {warn "Sorry could not define the Dancer2::Plugin::WebService installation directory\n"; exit 1}
  0            
  0            
103 0           print STDOUT "Application name : $plugin->{app}->{config}->{appname}\n";
104 0           print STDOUT "Application version : $plugin->{app}->{config}->{plugins}->{WebService}->{Version}\n";
105 0           print STDOUT "Run as user : ". (getpwuid($>))[0] ."\n";
106 0           print STDOUT "Started at : ". scalar(localtime $^T) ."\n";
107 0           print STDOUT "Process identifier : $$\n";
108 0           print STDOUT "WebService version : $VERSION\n";
109 0           print STDOUT "Module dir : $module_dir\n";
110              
111 0           foreach (keys %{$plugin->config->{'Authentication method'}} ) {
  0            
112 0 0         next unless $plugin->config->{'Authentication method'}->{$_}->{Active} =~/(?i)yes/;
113 0           $plugin->config->{'Authentication method'}->{$_}->{Command} =~s/^MODULE_INSTALL_DIR/$module_dir/;
114 0 0         if ( ! -f $plugin->config->{'Authentication method'}->{$_}->{Command} ) { warn 'Sorry, could not found the external authorization utility : "'. $plugin->config->{'Authentication method'}->{$_}->{Command} ."\"\n"; exit 1 }
  0            
  0            
115 0 0         if ( ! -x $plugin->config->{'Authentication method'}->{$_}->{Command} ) { warn 'Sorry, the external authorization utility "'. $plugin->config->{'Authentication method'}->{$_}->{Command} .'" is not executable from user '.getpwuid($>) ."\n"; exit 1 }
  0            
  0            
116 0           $plugin->Authentication_method($_);
117 0           print STDOUT "Authorization method : $_\n";
118 0           print STDOUT 'Authorization command: ', $plugin->config->{'Authentication method'}->{$_}->{Command} ,"\n";
119             last
120 0           }
121              
122 0 0         if ('' eq $plugin->Authentication_method) {
123 0           warn "\nCould not found any active authentication method, please check your configuration to activate at least one, and try again.\n";
124 0           exit 1
125             }
126              
127             # Search for stored sessions
128 0           $_ = $plugin->dir_session;
129 0           print STDOUT "Session idle time out: ". $plugin->Session_idle_timout ."\n";
130 0           print STDOUT "Ssession storage dir : $_\n";
131 0 0         opendir __SESSIONDIR, $_ or die "Could not list session directory $_ because $!\n";
132              
133 0           foreach my $session (grep ! /^\.+$/, readdir __SESSIONDIR) {
134 0 0         if (-f "$_/$session") {unlink "$_/$session"; next}
  0            
  0            
135              
136 0 0 0       if ((-f "$_/$session/__clientip") && (-f "$_/$session/__lastaccess") && (-f "$_/$session/__logintime") && (-f "$_/$session/__user")) {
      0        
      0        
137 0           my $lastaccess = ${ Storable::retrieve "$_/$session/__lastaccess" };
  0            
138              
139 0 0         if ( time - $lastaccess > $plugin->config->{'Session idle timout'} ) {
140 0           print STDOUT "Delete expired session: $session\n";
141 0           system $plugin->rm, '--recursive', '--force', "$_/$session"
142             }
143             else {
144             # Session is not expired update the __lastaccess and read the rest properties
145 0           print STDOUT "Found stored session : $session\n";
146 0 0         Storable::lock_store(\ time, "$_/$session/__lastaccess") or die "Could not store at session $session the property __lastaccess because $!\n"
147             }
148             }
149             else {
150 0           print STDERR "Delete corrupt session: $session\n";
151 0           system $plugin->rm, '--recursive', '--force', "$_/$session"
152             }
153             }
154              
155 0           closedir __SESSIONDIR;
156             #print STDERR "\n", Data::Dumper::Dumper( $plugin->from ) ,"\n\n";
157              
158              
159             # to reset the any posted or user data
160 0     0     $plugin->app->add_hook( Dancer2::Core::Hook->new(name => 'after', code => sub { $plugin->data_user({}) }) );
  0            
161              
162             #
163             $plugin->app->add_hook( Dancer2::Core::Hook->new(name => 'before', code => sub
164             {
165 0   0 0     $_ = (values %{ $plugin->app->request->params('route') })[0] // '';
  0            
166 0           $plugin->app->request->path =~/^\/*(.+?)\/*$_$/;
167 0           $plugin->route_name($^N);
168              
169              
170 0 0         $plugin->config->{Routes}->{ $plugin->route_name } = 'public' unless exists $plugin->config->{Routes}->{ $plugin->route_name }; # If a route is not defined at the configuration file we will considered it as public
171 0   0       $plugin->from($plugin->app->request->query_parameters->{from} // $plugin->config->{'Default format'});
172 0   0       $plugin->to( $plugin->app->request->query_parameters->{to} // $plugin->from);
173 0 0         if ( $plugin->from !~ $plugin->formats_regex ) { $plugin->error(20); $plugin->errormessage('property from '.$plugin->from.' is not one of the supported : '. join(', ',keys %{$plugin->formats})); $plugin->to('json'); $plugin->app->halt( $plugin->RestReply ) }
  0            
  0            
  0            
  0            
  0            
174 0 0         if ( $plugin->to !~ $plugin->formats_regex ) { $plugin->error(21); $plugin->errormessage('property to '. $plugin->to. ' is not one of the supported : '. join(', ',keys %{$plugin->formats})); $plugin->to('json'); $plugin->app->halt( $plugin->RestReply ) }
  0            
  0            
  0            
  0            
  0            
175              
176 0           $plugin->app->request->header('Content-Type'=> $plugin->formats->{$plugin->to}); # add header
177            
178             # Parse user's posted/sent data
179 0 0         if ( $plugin->app->request->body ) {
180 0           $plugin->data_from( $plugin->app->request->body );
181 0           my $hash = $plugin->__CONVERT_STRING_TO_HASHREF;
182              
183 0 0         if ( $plugin->error ) {
184 0           $plugin->to('json');
185 0           $plugin->dump_user_properties( { error=>$plugin->error, errormessage=>$plugin->errormessage, description=>'Data conversion error from '.$plugin->from.' to '.$plugin->to } );
186 0           $plugin->data_user( {} );
187 0 0         die "DataStructure internal error : ". $plugin->errormessage."\n" if $plugin->error;
188 0           $plugin->app->halt( $plugin->data_to )
189             }
190              
191 0           $plugin->data_user($hash)
192             }
193              
194             # Setup the remote IP address, even if the web service is running from a reverse proxy
195 0 0         $plugin->ClientIP( defined $plugin->app->request->env->{HTTP_X_REAL_IP} ? $plugin->app->request->env->{HTTP_X_REAL_IP} : defined $plugin->app->request->address ? $plugin->app->request->address : '127.0.0.1' );
    0          
196              
197 0 0         return if 'public' eq $plugin->config->{Routes}->{ $plugin->route_name };
198             # If the code gets to this line, we are sure, we are dealing with a private protected route
199              
200             # Check if the session is valid, or it is expired due to inactivity
201             # If the session is not expired update the __lastaccess
202 0 0         unless (exists $plugin->data_user->{SessionID}) { $plugin->error(2); $plugin->errormessage('You must login for using the private route '.$plugin->route_name); $plugin->data_user({description=>'Get SessionID via login route'}); $plugin->app->halt( $plugin->RestReply ) }
  0            
  0            
  0            
  0            
203 0           $_ = $plugin->dir_session.'/'.$plugin->data_user->{SessionID};
204 0 0         unless (-d $_) { $plugin->error(3); $plugin->errormessage('invalid or expired SessionID '.$plugin->data_user->{SessionID}); $plugin->data_user({description=>'Get a valid SessionID via login route'}); $plugin->app->halt( $plugin->RestReply ) }
  0            
  0            
  0            
  0            
205 0           my $lastaccess = ${ Storable::retrieve "$_/__lastaccess" };
  0            
206              
207 0 0         if ( time - $lastaccess > $plugin->config->{'Session idle timout'} ) {
208 0           $plugin->error(4);
209 0           $plugin->errormessage('Session '.$plugin->data_user->{SessionID}.' expired because its idle time '.(time - $lastaccess).' secs is more than the allowed '.$plugin->config->{'Session idle timout'}.' secs');
210 0           system $plugin->rm, '--recursive', '--force', $_;
211 0           $plugin->app->halt( $plugin->RestReply )
212             }
213             else {
214 0           Storable::lock_store(\ time, "$_/__lastaccess")
215             }
216 0           }));
217              
218              
219              
220              
221              
222             # Built in route /info
223 0     0     $plugin->app->add_route( regexp=> '/info', method=> 'get', code=> sub { $_[0]->forward('/info/version') } );
  0            
224              
225              
226             # Built in route /info/:what
227             $plugin->app->add_route(
228             method => 'get',
229             regexp => '/info/:what',
230             code => sub {
231 0     0     my $app= shift;
232              
233 0 0         if ( $app->request->param('what') =~/(?i)v/ ) {
    0          
234              
235             $plugin->RestReply(
236             Name => $plugin->app->{name},
237             Owner => $plugin->{app}->{config}->{plugins}->{WebService}->{Owner},
238 0 0         Os => eval{ $_='Posix'; if (open FILE, '/etc/issue') { ($_= )=~s/\v*$//m; close FILE} $_ },
  0            
  0            
  0            
  0            
239             'Service uptime secs' => time - $^T,
240             'Server date time' => scalar localtime time,
241             Version => {
242             Application => $plugin->{app}->{config}->{plugins}->{WebService}->{Version},
243             Dancer => $Dancer2::VERSION,
244             Perl => $],
245 0           'Linux kernel' => eval{$_ = qx/uname -r/; chomp $_; $_},
  0            
  0            
  0            
246             'WebService' => $VERSION
247             }
248             )
249             }
250             elsif ( $app->request->param('what') =~/(?i)cl/ ) {
251              
252             $plugin->RestReply(
253             'Client address' => $plugin->ClientIP,
254             'Client port' => $plugin->app->request->env->{REMOTE_PORT},
255 0           'Agent' => $plugin->app->request->agent,
256             'Is secure' => $plugin->app->request->secure,
257             'Protocol' => $plugin->app->request->protocol,
258             'Http method' => $plugin->app->request->method,
259             'Header accept' => $plugin->app->request->header('accept'),
260             'Parameters url' => join(' ', $plugin->app->request->params('query')),
261             'Parameters route' => join(' ', $plugin->app->request->params('route')),
262             'Parameters body' => join(' ', $plugin->app->request->params('body')) )
263             }
264             else {
265 0           $plugin->RestReply(error=>5, errormessage=>'Not existing internal route \''.$app->request->param('what').'\' Please choose one of : version, about, client')
266             }
267 0           } );
268              
269              
270             # logout and delete the session
271             $plugin->app->add_route(
272             method => $_,
273             regexp => '/logout',
274             code => sub
275             {
276 0     0     my $app = shift;
277 0           $plugin->error(0);
278 0           $plugin->errormessage('logged out from session '. $plugin->data_user->{SessionID} );
279 0           $plugin->__Delete_session;
280 0           $plugin->RestReply
281             }
282              
283 0           ) foreach 'get', 'post';
284              
285              
286              
287              
288             # curl -X GET --data '{"user":"Joe", "password":"MySecret" }' 'localhost:3000/login?from=json;to=json'
289             #
290             # Authenticate users using external custom scripts or commands
291             # using the appropriate shell script you can easily have your
292             # LDAP, kerberus, Active Directory, SQL, or what ever mechanism you want
293             # Feel free to write your own scripts and define them at config.yml
294             #
295             # The external custom shell authorization scripts/commands receives three arguments
296             #
297             # 1) user (as hex string)
298             # 2) password (as hex string)
299             # 3) comma delimited groups that the user should belong at least to one of them
300             #
301             # we convert the user, pass arguments to hex strings to avoid shell attacks.
302             # Remember at linux the maximum length of a shell command is getconf ARG_MAX
303             #
304             # The result is stored at
305             #
306             # $plugin->auth_result 1 for successful login, or 0 fail
307             # $plugin->auth_message the reason why the login was failed e.g "user do not exist"
308             # $plugin->auth_member_of In case of successful login, the groups that the user belongs (from the ones we have specify)
309             #
310             $plugin->app->add_route(
311             method => $_,
312             regexp => '/login',
313             code => sub
314             {
315 0     0     my $app = shift;
316              
317             # Check client IP address against the access rules
318 0           $plugin->error(13);
319 0           for (my $i=0; $i<@{ $plugin->rules_compiled }; $i++)
  0            
320             {
321 0 0         if ( $plugin->ClientIP =~ $plugin->rules_compiled->[$i] ) {
322 0           $plugin->error(0);
323 0           $plugin->errormessage('ok');
324 0           $plugin->data_user->{'IP access'} = 'Match client IP '. $plugin->ClientIP .' from rule '. $plugin->rules->[$i];
325             last
326 0           }
327             }
328              
329 0 0         if ( $plugin->error ) {
330 0           $plugin->errormessage('Client IP address '. $plugin->ClientIP .' is not allowed from any IP access rule');
331 0           $plugin->app->halt( $plugin->RestReply('user') )
332             }
333              
334             # Check the input parameters
335 0 0         foreach ('user','password') {unless (exists $plugin->data_user->{$_}) { $plugin->error(6); $plugin->errormessage("Login failed, you did not pass the $_"); $plugin->app->halt( $plugin->RestReply ) }}
  0            
  0            
  0            
  0            
336 0 0         if ( $plugin->data_user->{user} =~ /^\s*$/ ) { $plugin->error(7); $plugin->errormessage("Login failed because the user is blank"); $plugin->app->halt( $plugin->RestReply ) }
  0            
  0            
  0            
337 0 0         if ( $plugin->data_user->{password} eq '' ) { $plugin->error(8); $plugin->errormessage("Login failed because the password is blank"); $plugin->app->halt( $plugin->RestReply('user') ) }
  0            
  0            
  0            
338 0 0         if ( 0 == @{$plugin->groups } ) { $plugin->error(9); $plugin->errormessage("Login failed because the required group list is empty"); $plugin->app->halt( $plugin->RestReply('user') ) }
  0            
  0            
  0            
  0            
339              
340 0           $plugin->auth_result(0);
341 0           $plugin->auth_message('Unknown authentication error');
342 0           $plugin->auth_member_of([]);
343              
344 0           my $user = unpack 'H*', $plugin->data_user->{user};
345 0           my $password = unpack 'H*', $plugin->data_user->{password};
346 0           my $groups = join ',', @{$plugin->groups};
  0            
347 0           my $command = $plugin->config->{'Authentication method'}->{$plugin->Authentication_method}->{'Command'};
348 0           my @output = ();
349 0           $command = "\Q$command\E $user $password \Q$groups\E";
350 0 0         $command = $plugin->sudo ." $command" if $plugin->config->{'Authentication method'}->{$plugin->Authentication_method}->{'Use sudo'} =~/(?i)y/;
351              
352             #print "arguments after pack: $user $password $groups\n";
353             #print STDERR "\n*", $command ,"*\n\n";
354            
355             # Execute the external authorization utility and capture its 3 lines output at @output array
356 0 0         open SHELL, '-|', "$command 2> /dev/null" or die "Could run auth shell command \"$command\" because \"$?\"\n";
357 0           while () { s/^\s*(.*?)\s*$/$1/; push @output, $_ }
  0            
  0            
358 0           close SHELL;
359              
360 0           $plugin->auth_result( $output[0]);
361 0           $plugin->auth_message($output[1]);
362 0 0         $plugin->auth_member_of( [ split /,/, $output[2] ] ) if $plugin->auth_result;
363              
364 0 0         if ($plugin->auth_result) {
365 0 0         $plugin->auth_message('ok') if $plugin->auth_message eq '';
366 0 0         $plugin->auth_member_of(['emptylist']) unless @{ $plugin->auth_member_of }
  0            
367             }
368             else {
369 0 0         $plugin->auth_message('Unknown authentication error') if $plugin->auth_message eq '';
370 0           $plugin->auth_member_of([])
371             }
372              
373 0 0         $plugin->error( $plugin->auth_result == 0 ? 10 : 0 );
374 0           $plugin->errormessage( $plugin->auth_message );
375 0 0         $plugin->app->halt( $plugin->RestReply('user') ) if $plugin->error;
376              
377             # User authenticated successfully, now we must create his permanent session
378             # and store there some built in properties
379 0           my $SessionID = ''; $SessionID .= sprintf("%08x", int rand 800_000_000) for 1..4;
  0            
380              
381 0 0         if (-e $plugin->dir_session ."/$SessionID") {
382 0           my $i=1;
383 0           while ( -e $plugin->dir_session ."/$i.$SessionID" ) {$i++}
  0            
384 0           $SessionID = "$i.$SessionID"
385             }
386              
387 0 0         unless (mkdir $plugin->dir_session ."/$SessionID") {
388 0           $plugin->error(12);
389 0           $plugin->errormessage("Login failed . Could not create session directory $SessionID because $!");
390 0           $plugin->app->halt( $plugin->RestReply('user') )
391             }
392            
393 0           $plugin->data_user->{SessionID} = $SessionID;
394 0           $plugin->set_data_session('__clientip'=> $plugin->ClientIP, '__lastaccess'=> time, '__logintime'=> time, '__user'=> $plugin->data_user->{user});
395              
396             $plugin->RestReply(
397             'IP access' => $plugin->data_user->{'IP access'},
398             'user' => $plugin->data_user->{user},
399             'SessionID' => $SessionID,
400 0           'Max idle seconds' => $plugin->config->{'Session idle timout'},
401             'auth_message' => $plugin->auth_message,
402             'auth_member_of' => $plugin->auth_member_of )
403             }
404              
405 0           )foreach 'get', 'post';
406              
407              
408             #print STDERR "\n*". Data::Dumper::Dumper( $plugin ) ."*\n\n";
409             #print STDERR "\n*". $plugin->config->{Routes} ."*\n\n";
410             }
411              
412              
413              
414 1     1   560 use JSON::XS;
  1         2363  
  1         97  
415             my $obj_json = JSON::XS->new;
416             $obj_json->utf8(1);
417             $obj_json->max_depth(1024);
418             $obj_json->indent(1);
419             $obj_json->pretty(1);
420             $obj_json->space_before(0);
421             $obj_json->space_after(0);
422             $obj_json->max_size(0);
423             $obj_json->relaxed(0);
424             $obj_json->shrink(0);
425             $obj_json->allow_tags(1);
426             $obj_json->allow_nonref(0);
427             $obj_json->allow_unknown(0);
428             $obj_json->allow_blessed(1);
429             $obj_json->convert_blessed(1);
430              
431 1     1   484 use XML::Hash::XS;
  1         659  
  1         65  
432             $XML::Hash::XS::root='Data';
433             $XML::Hash::XS::utf8=1;
434             $XML::Hash::XS::encoding='utf8';
435             $XML::Hash::XS::xml_decl=0;
436             $XML::Hash::XS::indent=2;
437             $XML::Hash::XS::canonical=1;
438             $XML::Hash::XS::doc=0;
439             $XML::Hash::XS::version='1.1';
440              
441 1     1   357 use YAML::XS;
  1         1769  
  1         48  
442             $YAML::XS::QuoteNumericStrings=1;
443              
444 1     1   497 use Data::Dumper;
  1         4804  
  1         2255  
445             $Data::Dumper::Terse=1;
446             $Data::Dumper::Purity=1;
447             $Data::Dumper::Indent=2;
448             $Data::Dumper::Deepcopy=1;
449             $Data::Dumper::Trailingcomma=0;
450              
451              
452             # This is my custom Perl Data Structures recursive walker
453             # it is usefull when you want to view a Complex data structure at human format
454             my %Handler;
455             %Handler =
456             (
457             SCALAR => sub { $Handler{WALKER}->(${$_[0]}, $_[1], @{$_[2]} )},
458             ARRAY => sub { $Handler{WALKER}->($_, $_[1], @{$_[2]}) for @{$_[0]} },
459             HASH => sub { $Handler{WALKER}->($_[0]->{$_}, $_[1], @{$_[2]}, $_) for sort keys %{$_[0]} },
460             '' => sub { $_[1]->($_[0], @{$_[2]}) },
461             WALKER => sub { my $data = shift; $Handler{ref $data}->($data, shift, \@_) }
462             );
463              
464              
465              
466              
467              
468              
469             # Convert a string ( data_from ) to a Perl hash reference
470             # as the $obj->{from} defines : json, xml, yaml, perl, human
471             #
472             sub __CONVERT_STRING_TO_HASHREF
473             {
474 0     0     my $obj = $_[0];
475 0           @{$obj}{qw/error errormessage/}=(0,'ok');
  0            
476              
477 0 0 0       if (( ! defined $obj->{data_from} ) || ( $obj->{data_from} =~/^\s*$/ )) {
478 0           @{$obj}{qw/error errormessage/} = (1, "There are not any data to convert at property data_from");
  0            
479             return {}
480 0           }
481              
482 0           my $hash={};
483              
484 0           eval {
485 0 0         if ( $obj->{from} eq 'json' ) { $hash = JSON::XS::decode_json $obj->{data_from} }
  0 0          
    0          
    0          
    0          
486 0           elsif ( $obj->{from} eq 'xml' ) { $hash = XML::Hash::XS::xml2hash $obj->{data_from} }
487 0           elsif ( $obj->{from} eq 'yaml' ) { $hash = YAML::XS::Load $obj->{data_from} }
488 0           elsif ( $obj->{from} eq 'perl' ) { $hash = eval $obj->{data_from} }
489 0           elsif ( $obj->{from} eq 'human') { my $arrayref;
490              
491 0           while ( $obj->{data_from} =~/(.*)$/gm ) {
492 0           my @array = split /\s*(?:\,| |\t|-->|==>|=>|->|=|;|\|)+\s*/, $1;
493 0 0         next unless @array;
494              
495 0 0         if (@array % 2 == 0) {
496 0           push @{$arrayref}, { @array }
  0            
497             }
498             else {
499 0           push @{$arrayref}, { shift @array => [ @array ] }
  0            
500             }
501             }
502              
503 0 0         $hash = 1==scalar @{$arrayref} ? $arrayref->[0] : { 'Data' => $arrayref }
  0            
504             }
505             };
506              
507 0 0         if ($@) {
508 0           $hash={};
509 0           $obj->{error}=1;
510 0           ($obj->{errormessage}="The data parsing as $obj->{from} failed. Are you sure your data are at $obj->{from} format ? The low level error is : $@") =~s/[\v\h]+/ /g
511             }
512             $hash
513 0           }
514              
515              
516              
517             # Convert hash reference $_[0] to text and store it at $obj->{data_to}
518             # format of "data_to" is depended from "to" : json xml yaml perl human
519             #
520             # __CONVERT_HASHREF_TO_STRING( $hash_reference )
521             # print $obj->{error} ? "ERROR : $obj->{errormessage}" : $obj->{data_to};
522             #
523             sub __CONVERT_HASHREF_TO_STRING
524             {
525 0     0     my $obj=shift;
526 0           @{$obj}{qw/error errormessage/}=(0,'ok');
  0            
527 0           $obj->{data_to}='';
528              
529 0           eval {
530 0 0         if ($obj->{to} eq 'json' ) { $obj->{data_to} = $obj_json->encode($_[0]) }
  0 0          
    0          
    0          
    0          
531 0           elsif ($obj->{to} eq 'xml' ) { $obj->{data_to} = XML::Hash::XS::hash2xml $_[0] }
532 0           elsif ($obj->{to} eq 'yaml' ) { $obj->{data_to} = YAML::XS::Dump $_[0] }
533 0           elsif ($obj->{to} eq 'perl' ) { $obj->{data_to} = Data::Dumper::Dumper $_[0] }
534 0     0     elsif ($obj->{to} eq 'human') { $Handler{WALKER}->($_[0], sub {my $val=shift; $val =~s/^\s*(.*?)\s*$/$1/; $obj->{data_to} .= join('.', @_) ." = $val\n"}) }
  0            
  0            
  0            
535             };
536              
537 0 0         if ($@) {
538 0           @{$obj}{qw/data_to error errormessage/}=('', 1, "The encoding of data hash to $obj->{to} failed. The low level error is : $@");
  0            
539 0           $obj->{errormessage} =~s/[\v\h]+/ /g
540             }
541              
542             $obj->{data_to}
543 0           }
544              
545              
546              
547              
548              
549              
550             # Returns a reply as: json, xml, yaml, perl or human
551             # It always include the error and errormessage
552             #
553             # RestReply error and errormessage
554             # RestReply(k1 => 'v1', ...) specific key/values
555             # RestReply('DATA_USER_SEND') send data
556             # RestReply('DATA_USER_ALL') send data and defined key/value by the user
557             #
558             sub RestReply
559             {
560 0     0 1   my $plugin = shift;
561              
562 0 0         if (@_) {
563              
564 0 0         if (1 == @_) {
565              
566 0 0 0       if (('DATA_USER_SEND' eq $_[0]) || ('DATA_USER_ALL' eq $_[0])) {
567 0           $plugin->dump_user_properties($_[0])
568             }
569             else {
570 0 0         $plugin->dump_user_properties({ error=> $plugin->error, errormessage=> $plugin->errormessage, $_[0]=> exists $plugin->data_user->{$_[0]} ? $plugin->data_user->{$_[0]} : 'NOT EXISTING USER DATA' })
571             }
572             }
573             else {
574             # This the normal operation
575 0           $plugin->dump_user_properties( {error=> $plugin->error, errormessage=> $plugin->errormessage, @_} )
576             }
577             }
578             else {
579             # if no argument passed then we return only error, errormessage and if exists description
580 0 0         $plugin->dump_user_properties({ error=>$plugin->error, errormessage=>$plugin->errormessage, exists $plugin->data_user->{description} ? ( 'description' , $plugin->data_user->{description} ) : () })
581             }
582              
583 0 0         if ( $plugin->error ) {
584 0           $plugin->to('json');
585 0           $plugin->dump_user_properties( { error=>$plugin->error, errormessage=>$plugin->errormessage, description=>'Data conversion error from '.$plugin->from.' to '.$plugin->to } );
586 0           $plugin->data_user( {} );
587 0 0         die "DataStructure internal error : ". $plugin->errormessage."\n" if $plugin->error;
588 0           $plugin->app->halt( $plugin->data_to )
589             }
590              
591             $plugin->data_to
592 0           }
593              
594              
595              
596              
597              
598             # $plugin->dump_user_properties( { k1 => 'v1', ... } ); # specific key/values
599             # $plugin->dump_user_properties( 'DATA_USER_SEND' ); # send data
600             # $plugin->dump_user_properties( 'DATA_USER_ALL' ); # send data and defined key/value by the user
601             #
602             # Answer is a string formatted as $plugin->to( json|yaml|xml|perl|human)
603             # and stored at $plugin->data_to
604             #
605             sub dump_user_properties
606             {
607 0     0 0   my $plugin = shift;
608 0           my $hash = {};
609 0           $plugin->data_to('');
610              
611             # specific user data
612 0 0         if ('HASH' eq ref $_[0])
    0          
    0          
613             {
614 0           $plugin->__CONVERT_HASHREF_TO_STRING($_[0])
615             }
616              
617             # user data (all)
618             elsif ('DATA_USER_ALL' eq $_[0])
619             {
620 0           $hash->{error} = $plugin->error;
621 0           $hash->{errormessage} = $plugin->errormessage;
622 0           map { $hash->{$_} = $plugin->data_user->{$_} } keys %{ $plugin->data_user };
  0            
  0            
623 0           $plugin->__CONVERT_HASHREF_TO_STRING($hash)
624             }
625              
626             # only the send data
627             elsif ('DATA_USER_SEND' eq $_[0])
628             {
629 0 0         if ($plugin->from eq $plugin->to)
630             {
631 0           $_= $plugin->data_from;
632 0           s/^\s*(.*?)\s*$/$1/s;
633 0           $plugin->data_to($_)
634             }
635             else
636             {
637 0           $hash = $plugin->__CONVERT_STRING_TO_HASHREF; # whatever exists in $plugin->data_from in any format make it hash
638 0           $plugin->__CONVERT_HASHREF_TO_STRING($hash)
639             }
640             }
641              
642             $plugin->data_to
643 0           }
644              
645              
646             # Create nested directories like the mdkir -p ...
647             #
648             sub __MKDIR {
649 0     0     my @Mkdir = split /(?:\\|\/)+/, $_[0];
650 0 0         return $_[0] unless @Mkdir;
651 0 0 0       splice(@Mkdir, 0, 2, "/$Mkdir[1]") if (($Mkdir[0] eq '') && (scalar @Mkdir > 0));
652 0           my $i;
653              
654 0           for($i=$#Mkdir; $i>=0; $i--) {
655 0 0         last if -d join '/', @Mkdir[0..$i]
656             }
657              
658 0           for(my $j=$i+1; $j<=$#Mkdir; $j++) {
659 0 0         mkdir join('/', @Mkdir[0 .. $j]) or return undef
660             }
661 0           $_[0]
662             }
663              
664              
665             # Delete session directory and property
666             #
667             sub __Delete_session {
668 0     0     my $plugin = shift;
669 0           my $dir = $plugin->dir_session.'/'.$plugin->data_user->{SessionID};
670              
671 0 0         if (-d $dir) {
672 0           my $exit_code = system $plugin->rm, '--recursive', '--force', $dir;
673 0 0         if ($exit_code) { $plugin->error(11); $plugin->errormessage('Could not delete session '. $plugin->data_user->{SessionID} ." because $!") }
  0            
  0            
674             }
675             }
676              
677              
678             # Returns the posted or sent data
679             #
680             # my ($var1, $var2) = get_data_user('k1', 'k2'); # returns the selected keys
681             # my %hash = get_data_user(); # returns all data as hash
682             #
683             sub get_data_user
684             {
685 0     0 1   my $plugin = shift;
686              
687 0 0         if (@_) {
688 0 0         map {exists $plugin->data_user->{$_} ? $plugin->data_user->{$_} : 'NOT EXISTING USER DATA'} @_
  0            
689             }
690             else {
691 0           %{ $plugin->data_user }
  0            
692             }
693             }
694              
695              
696              
697             # Set new user data as if they were sent or posted
698             # It returns the data like the get_user_data
699             #
700             # my %data = set_data_user( new1 => 'foo1', new2 => 'foo2' ); # return the keys
701             # my %data = set_data_user( { new1 => 'foo1', new2 => 'foo2' } ); # return the keys
702             #
703             sub set_data_user
704             {
705 0     0 1   my $plugin = shift;
706 0           my @keys;
707              
708 0 0         if (@_)
709             {
710 0 0 0       if (( 1 == @_ ) && ( 'HASH' eq ref $_[0] )) { @_ = %{ $_[0] } }
  0            
  0            
711              
712 0           for (my($i,$j)=(0,1); $i < scalar(@_) - (scalar(@_) % 2); $i+=2,$j+=2)
713             {
714 0           push @keys, $_[$i];
715 0           $plugin->data_user->{$_[$i]} = $_[$j]
716             }
717              
718 0           %{ $plugin->data_user }{ @keys }
  0            
719             }
720             }
721              
722              
723             # Delete user data
724             #
725             # del_data_user( 'k1', 'k2', ... ); # delete only the selected keys
726             # del_data_user(); # delete all keys
727             #
728             sub del_data_user
729             {
730 0     0 1   my $plugin = shift;
731              
732 0 0         if (@_) {
733              
734 0           foreach (@_) {
735 0 0         delete $plugin->data_user->{$_} if exists $plugin->data_user->{$_}
736             }
737             }
738             else {
739 0           $plugin->data_user({})
740             }
741             }
742              
743              
744              
745             # Retrieves stored session data
746             #
747             # my %data = get_data_session( 'k1', 'k2', ... ); # return only the selected keys
748             # my %data = get_data_session(); # returs all keys
749             #
750             sub get_data_session
751             {
752 0     0 1   my $plugin = shift;
753 0 0         unless ( exists $plugin->data_user->{SessionID} ) { $plugin->error(2); $plugin->errormessage('You must login for using persistent session data'); $plugin->data_user({description=>'Get SessionID via login route'}); $plugin->app->halt( $plugin->RestReply ) }
  0            
  0            
  0            
  0            
754 0           my $id = $plugin->data_user->{SessionID};
755 0           my $dir = $plugin->dir_session."/$id";
756 0 0         unless (-d $dir) { $plugin->error(3); $plugin->errormessage("Invalid or expired SessionID $id"); $plugin->data_user({description=>'Get a valid SessionID via login route'}); $plugin->app->halt( $plugin->RestReply ) }
  0            
  0            
  0            
  0            
757              
758 0           my %hash;
759              
760 0 0         if (@_)
761             {
762 0           foreach (@_)
763             {
764 0 0         if ( ! -f "$dir/$_" ) { $hash{$_} = "NOT EXISTING SESSION RECORD $_"; next }
  0            
  0            
765              
766 0 0         if ( $hash{$_} = Storable::retrieve "$dir/$_" )
767             {
768 0 0         $hash{$_} = ${ $hash{$_} } if 'SCALAR' eq ref $hash{$_}
  0            
769             }
770             else
771             {
772 0           $plugin->error(1);
773 0           $plugin->errormessage("Could not retrieve from session $id the property $_ because $!");
774 0           $plugin->app->halt( $plugin->RestReply('error') )
775             }
776             }
777              
778 0           map { $hash{$_} } @_
  0            
779             }
780             else
781             {
782 0           opendir __SESSIONDIR, $dir;
783              
784 0           foreach (grep ! /^\.+$/, readdir __SESSIONDIR)
785             {
786 0 0         next if -d "$dir/$_";
787              
788 0 0         if ( $hash{$_} = Storable::retrieve "$dir/$_" )
789             {
790 0 0         $hash{$_} = ${ $hash{$_} } if 'SCALAR' eq ref $hash{$_}
  0            
791             }
792             else
793             {
794 0           $plugin->error(1);
795 0           $plugin->errormessage("Could not retrieve from session $id the property $_ because $!");
796 0           $plugin->app->halt( $plugin->RestReply() )
797             }
798             }
799              
800 0           closedir __SESSIONDIR;
801 0           %hash
802             }
803             }
804              
805              
806              
807              
808             # Set and store session data
809             # Session data are not volatile like the user data.
810             # They are persistent between requests
811             #
812             # set_data_session( new1 => 'foo1', new2 => 'foo2' );
813             # set_data_session( { new1 => 'foo1', new2 => 'foo2' } );
814             #
815             sub set_data_session
816             {
817 0     0 1   my $plugin = shift;
818 0 0         unless ( exists $plugin->data_user->{SessionID} ) { $plugin->error(2); $plugin->errormessage('You must login for using persistent session data'); $plugin->data_user({description=>'Get SessionID via login route'}); $plugin->app->halt( $plugin->RestReply ) }
  0            
  0            
  0            
  0            
819 0           my $id = $plugin->data_user->{SessionID};
820 0           my $dir = $plugin->dir_session."/$id";
821 0 0         unless (-d $dir) { $plugin->error(3); $plugin->errormessage("Invalid or expired SessionID $id"); $plugin->data_user({description=>'Get a valid SessionID via login route'}); $plugin->app->halt( $plugin->RestReply ) }
  0            
  0            
  0            
  0            
822              
823 0 0 0       if (( 1 == @_ ) && ( 'HASH' eq ref $_[0] )) { @_ = %{ $_[0] } }
  0            
  0            
824              
825 0           for (my($i,$j)=(0,1); $i < scalar(@_) - (scalar(@_) % 2); $i+=2,$j+=2)
826             {
827 0           my $data = $_[$j];
828 0 0         $data = \ "$data" unless ref $data;
829              
830 0 0         unless ( Storable::lock_store $data, "$dir/$_[$i]" )
831             {
832 0           $plugin->error(1);
833 0           $plugin->errormessage("Could not store at session $id the property $_[$i] because $!");
834 0           $plugin->app->halt( $plugin->RestReply )
835             }
836             }
837             }
838              
839              
840              
841              
842             # Delete session data (not sessions)
843             # It never deletes the built in records : __lastaccess, __logintime, __clientip, __user
844             #
845             # del_data_session( 'k1', 'k2', ... ); # delete only the selected keys
846             # del_data_session(); # delete all keys
847             #
848             sub del_data_session
849             {
850 0     0 1   my $plugin = shift;
851 0 0         unless (exists $plugin->data_user->{SessionID}) { $plugin->error(2); $plugin->errormessage('You must login for using persistent session data'); $plugin->data_user({description=>'Get SessionID via login route'}); $plugin->app->halt( $plugin->RestReply ) }
  0            
  0            
  0            
  0            
852              
853 0           my $dir = $plugin->dir_session.'/'.$plugin->data_user->{SessionID};
854 0 0         unless (-d $dir) { $plugin->error(3); $plugin->errormessage('invalid or expired SessionID '.$plugin->data_user->{SessionID}); $plugin->data_user({description=>'Get a valid SessionID via login route'}); $plugin->app->halt( $plugin->RestReply ) }
  0            
  0            
  0            
  0            
855              
856 0 0         if (@_) {
857              
858 0           foreach (@_) {
859 0 0         next if /^__logintime|__lastaccess|__user|__clientip$/;
860 0 0         next unless -f "$dir/$_";
861 0 0         unless (unlink "$dir/$_") { $plugin->error(5); $plugin->errormessage('Could not delete from session '.$plugin->data_user->{SessionID}." the record $_ because $!"); $plugin->app->halt( $plugin->RestReply ) }
  0            
  0            
  0            
862             }
863             }
864             else {
865 0           opendir __SESSIONDIR, $dir;
866              
867 0           foreach (grep ! /^\.+$/, readdir __SESSIONDIR) {
868 0 0         next if /^__logintime|__lastaccess|__user|__clientip$/;
869 0 0         next unless -f "$dir/$_";
870 0 0         unless (unlink "$dir/$_") { $plugin->error(5); $plugin->errormessage('Could not delete from session '.$plugin->data_user->{SessionID}." the record $_ because $!"); $plugin->app->halt( $plugin->RestReply ) }
  0            
  0            
  0            
871             }
872              
873 0           closedir __SESSIONDIR
874             }
875             }
876              
877              
878              
879              
880             1;
881             __END__