File Coverage

blib/lib/Dancer2/Plugin/WebService.pm
Criterion Covered Total %
statement 45 504 8.9
branch 0 278 0.0
condition 0 73 0.0
subroutine 15 31 48.3
pod 6 7 85.7
total 66 893 7.3


line stmt bran cond sub pod time code
1             # ABSTRACT: Rest APIs with login, persistent data, multiple in/out formats, IP security, role based access
2             # Multiple input/output formats : json , xml , yaml, perl , human
3             #
4             # George Bouras , george.mpouras@yandex.com
5             # Joan Ntzougani, ✞
6              
7             package Dancer2::Plugin::WebService;
8             our $VERSION = '4.8.8';
9             if ( $^O =~/(?i)MSWin/ ) { CORE::warn "\nOperating system is not supported\n"; CORE::exit 1 }
10              
11 1     1   91579 use strict;
  1         2  
  1         34  
12 1     1   4 use warnings;
  1         2  
  1         60  
13 1     1   494 use Encode;
  1         15223  
  1         130  
14 1     1   701 use Dancer2::Plugin;
  1         277729  
  1         13  
15 1     1   50458 use Storable;
  1         1  
  1         119  
16 1     1   731 use Data::Dumper; $Data::Dumper::Sortkeys=0; $Data::Dumper::Indent=1; $Data::Dumper::Terse=1; $Data::Dumper::Deepcopy=1; $Data::Dumper::Purity=1; $Data::Dumper::Useperl=0; $Data::Dumper::Trailingcomma=0;
  1         6147  
  1         156  
17 1     1   738 use XML::Hash::XS; my $XML = XML::Hash::XS->new( utf8=>1, indent=>0, canonical=>0, encoding=>'utf-8', root=>'root', xml_decl=>0);
  1         1236  
  1         88  
18 1     1   980 use Cpanel::JSON::XS; my $JSON = Cpanel::JSON::XS->new; $JSON->utf8(1); $JSON->indent(0); $JSON->canonical(0); $JSON->pretty(0); $JSON->max_size(0); $JSON->space_before(0); $JSON->space_after(1); $JSON->relaxed(0); $JSON->allow_tags(1); $JSON->allow_unknown(0); $JSON->shrink(0); $JSON->allow_nonref(0); $JSON->allow_blessed(0); $JSON->convert_blessed(0); $JSON->max_depth(1024);
  1         2274  
  1         117  
19 1     1   580 use YAML::XS; my $YAML = YAML::XS->new( utf8=>0, indent=>2, header=>1, footer=>0, width=>2048, anchor_prefix=>'');
  1         2684  
  1         8265  
20              
21              
22             my %Formats = (json=>'application/json', xml=>'application/xml', yaml=>'application/yaml', perl=>'text/plain', human=>'text/plain');
23             my $fmt_rgx = eval 'qr/^('. join('|', sort keys %Formats) .')$/';
24             my $dir;
25             my $tmp;
26             my %Handler;
27             my %TokenDB;
28             my @keys;
29              
30             has token => (is=>'rw', lazy=>1, default => undef);
31             has error => (is=>'rw', lazy=>1, default => 0);
32             has sort => (is=>'rw', lazy=>1, default => 0);
33             has pretty => (is=>'rw', lazy=>1, default => 1);
34             has route_name => (is=>'rw', lazy=>1, default => '');
35             has ClientIP => (is=>'rw', lazy=>1, default => '');
36             has reply_text => (is=>'rw', lazy=>1, default => '');
37             has auth_method => (is=>'rw', lazy=>1, default => '');
38             has auth_command => (is=>'rw', lazy=>1, default => '');
39             has data => (is=>'rw', lazy=>1, default => ''); # user posted data
40             has auth_config => (is=>'rw', lazy=>1, default => sub{ {} });
41             has Format => (is=>'rw', lazy=>1, default => sub{ {from => undef, to => undef} });
42             has Session_timeout => (is=>'ro', lazy=>0, from_config=> 'Session idle timeout',default=> sub{ 3600 }, isa => sub {unless ( $_[0]=~/^\d+$/ ) {warn "Session idle timeout \"$_[0]\" It is not a number\n"; exit 1}} );
43             has rules => (is=>'ro', lazy=>0, from_config=> 'Allowed hosts', default=> sub{ ['127.*', '192.168.*', '172.16.*'] });
44             has rules_compiled => (is=>'ro', lazy=>0, default => sub {my $array = [@{$_[0]->rules}]; for (@{$array}) { s/([^?*]+)/\Q$1\E/g; s|\?|.|g; s|\*+|.*?|g; $_ = qr/^$_$/i } $array});
45             has dir_session => (is=>'ro', lazy=>0, default => sub {my $D = exists $_[0]->config->{'Session directory'} ? $_[0]->config->{'Session directory'}."/$_[0]->{app}->{name}" : "$_[0]->{app}->{config}->{appdir}/session"; $D=~s|/+|/|g; my @MD = split /(?:\\|\/)+/, $D; my $i; for ($i=$#MD; $i>=0; $i--) { last if -d join '/', @MD[0..$i] } for (my $j=$i+1; $j<=$#MD; $j++) { unless (mkdir join '/', @MD[0 .. $j]) {warn "Could not create the session directory \"$D\" because $!\n"; exit 1} } $D} );
46             has OS => (is=>'ro', lazy=>0, default => sub {my $D = undef; foreach (qw[/usr/bin /bin /usr/sbin /sbin]) {if (-f "$_/uname") {$D="$_/uname"; last}; unless (defined $D) {warn "Could not found utility uname\n"; exit 1} } sub{-f $_[0] ? sub{open __F, $_[0]; $_=readline __F; close __F; ($_) = $_=~ /\A(\S+\s+\S+\s+\S+).*/ ; $_}->($_[0]) : sub { $_=qx[$D -sr] ; chomp ; $_ }->() }->('/proc/version') });
47             has rm => (is=>'ro', lazy=>0, default => sub {foreach (qw[/usr/bin /bin /usr/sbin /sbin]) {return "$_/rm" if -f "$_/rm" && -x "$_/rm" } warn "Could not found utility rm\n"; exit 1});
48             has session_enable => (is=>'ro', lazy=>0, default => sub {exists $_[0]->config->{'Session enable'} ? $_[0]->config->{'Session enable'}=~/(?i)[y1t]/ ? 1:0 : 1});
49              
50             # Recursive walker of complex and custon Data Structures
51             %Handler=(
52             SCALAR => sub { $Handler{WALKER}->(${$_[0]}, $_[1], @{$_[2]} )},
53             ARRAY => sub { $Handler{WALKER}->($_, $_[1], @{$_[2]}) for @{$_[0]} },
54             HASH => sub { $Handler{WALKER}->($_[0]->{$_}, $_[1], @{$_[2]}, $_) for sort keys %{$_[0]} },
55             '' => sub { $_[1]->($_[0], @{$_[2]}) },
56             WALKER => sub { my $data = shift; $Handler{ref $data}->($data, shift, \@_) }
57             );
58              
59              
60             sub BUILD
61             {
62 0     0 0   my $plg = shift;
63 0           my $app = $plg->app;
64              
65 0           (my $module_dir =__FILE__) =~s|/[^/]+$||; # Module's directory
66 0 0         unless (-d $module_dir) { CORE::warn "Could not find the Dancer2::Plugin::WebService installation directory\n"; CORE::exit 1 }
  0            
  0            
67              
68             # Built-in routes and their security
69 0           $plg->config->{Routes}->{logout} = { Protected => 1, 'Built in' => 1, Groups=>[] }; # we should be logged in to logout
70 0           $plg->config->{Routes}->{login} = { Protected => 0, 'Built in' => 1 };
71 0           $plg->config->{Routes}->{WebService} = { Protected => 0, 'Built in' => 1 };
72 0           $plg->config->{Routes}->{'WebService/client'} = { Protected => 0, 'Built in' => 1 };
73 0           $plg->config->{Routes}->{'WebService/routes'} = { Protected => 0, 'Built in' => 1 };
74 0           $plg->config->{Routes}->{''} = { Protected => 2, 'Built in' => 1 };
75              
76             # Default settings
77 0 0 0       $plg->config->{'Default format'}= 'json' if ((! exists $plg->config->{'Default format'}) || ($plg->config->{'Default format'} !~ $fmt_rgx));
78 0           $app->config->{content_type} = $Formats{ $plg->config->{'Default format'} };
79 0   0       $app->config->{show_errors} //= 0;
80 0   0       $app->config->{charset} //= 'UTF-8';
81 0   0       $app->config->{encoding} //= 'UTF-8';
82              
83             # Use the first active authentication method
84 0           foreach my $method (@{$plg->config->{'Authentication methods'}}) {
  0            
85 0 0 0       next unless ((exists $method->{Active}) && ($method->{Active}=~/(?i)[y1t]/));
86 0           $plg->auth_method( $method->{Name} );
87              
88             # If the Authorization method is an external script
89 0 0         if ($plg->auth_method ne 'INTERNAL') {
90 0 0         unless (exists $method->{Command}) {warn "The active Authentication method \"".$plg->auth_method."\" does not know what to do\n"; exit 1}
  0            
  0            
91 0           $method->{Command} =~s/^MODULE_INSTALL_DIR/$module_dir/;
92 0 0         unless (-f $method->{Command}) {warn "Sorry, could not found the external authorization utility $method->{Command}\n"; exit 1}
  0            
  0            
93 0 0         unless (-x $method->{Command}) {warn "Sorry, the external authorization utility $method->{Command} is not executable from user ". getpwuid($>) ."\n"; exit 1}
  0            
  0            
94              
95 0 0 0       if ((exists $method->{'Use sudo'}) && ($method->{'Use sudo'}=~/(?i)[y1t]/)) {
96 0           my $sudo = undef;
97 0 0 0       foreach (qw[/usr/bin /bin /usr/sbin /sbin]) { if ((-f "$_/sudo") && -x ("$_/sudo")) { $sudo="$_/sudo"; last } }
  0            
  0            
  0            
98 0 0         unless (defined $sudo) {warn "Could not found sudo command\n"; exit 1}
  0            
  0            
99 0           $plg->auth_command( "$sudo \Q$method->{Command}\E" )
100             }
101             else {
102 0           $plg->auth_command( "\Q$method->{Command}\E" )
103             }
104             }
105              
106 0           delete @{$method}{'Name','Active','Command','Use sudo'};
  0            
107 0   0       $method->{Arguments} //= [];
108 0           $plg->auth_config($method);
109             last
110 0           }
111              
112 0           delete $plg->config->{'Session enable'};
113 0           delete $plg->config->{'Authentication methods'};
114              
115 0 0 0       if (($plg->session_enable) && ($plg->auth_method eq '')) {
116 0           warn "\nWhile the sessions are enabled there is not any active authorization method at your config.yml\n";
117 0           CORE::exit 1
118             }
119              
120             # Check if there are protected routes
121 0           foreach (keys %{$plg->config->{Routes}}) {
  0            
122 0 0         next if exists $plg->config->{Routes}->{$_}->{'Built in'};
123 0           $plg->config->{Routes}->{$_}->{'Built in'}=0;
124              
125 0 0 0       if ((exists $plg->config->{Routes}->{$_}->{Protected}) && ($plg->config->{Routes}->{$_}->{Protected}=~/(?i)[y1t]/)) {
126              
127 0           delete $plg->config->{Routes}->{$_}->{Protected};
128 0           $plg->config->{Routes}->{$_}->{Protected}=1;
129              
130 0 0         if ($plg->auth_method eq '') {
131 0           warn "\nWhile there is at least one protected route ( $_ ) there is not any active authorization method at your config.yaml\n";
132 0           CORE::exit 1
133             }
134             else {
135              
136 0 0         if (exists $plg->config->{Routes}->{$_}->{Groups}) {
137             $plg->config->{Routes}->{$_}->{Groups} = [ $plg->config->{Routes}->{$_}->{Groups} ] unless 'ARRAY' eq ref $plg->config->{Routes}->{$_}->{Groups}
138 0 0         }
139             else {
140 0           $plg->config->{Routes}->{$_}->{Groups} = []
141             }
142             }
143             }
144             else {
145 0           delete $plg->config->{Routes}->{$_}->{Protected};
146 0           $plg->config->{Routes}->{$_}->{Protected}=0
147             }
148             }
149              
150 0           print STDOUT "\n";
151 0           print STDOUT "Application name : ", $plg->dsl->config->{appname} ,"\n";
152 0           print STDOUT 'Start time : ', scalar localtime $^T ,"\n";
153 0           print STDOUT 'Run as user : ', (getpwuid($>))[0] ,"\n";
154 0           print STDOUT "Command : $0\n";
155 0           print STDOUT "PID parent : ", getppid() ,"\n";
156 0           print STDOUT "PID Main : $$\n";
157 0 0         print STDOUT 'Authorization method : ', ( $plg->auth_method ? $plg->auth_method :'UNDEFINED' ) ,"\n";
158 0           print STDOUT "Authorization scripts : $module_dir/\n";
159 0           print STDOUT 'Environment : ', $plg->dsl->config->{environment} ,"\n";
160 0           print STDOUT 'Logging : ', $plg->dsl->config->{log} ,"\n";
161 0 0         print STDOUT 'Session enable : ', ( $plg->session_enable ? 'Yes' : 'No') ,"\n";
162 0           print STDOUT 'Session directory : ', $plg->dir_session ,"\n";
163 0           print STDOUT 'Session idle timeout : ', $plg->Session_timeout ," sec\n";
164 0 0         print STDOUT "Version application : ", ( exists $plg->dsl->config->{appversion} ? $plg->dsl->config->{appversion} : '0.0.0' ) ,"\n";
165 0           print STDOUT "Version Perl : $^V\n";
166 0           print STDOUT "Version Dancer2 : $Dancer2::VERSION\n";
167 0           print STDOUT "Version WebService : $VERSION\n";
168 0           print STDOUT "Operating system : ", $plg->OS ,"\n";
169              
170             # Restore the valid sessions, and delete the expired ones
171 0 0         opendir DIR, $plg->dir_session or die "Could not list session directory $plg->{dir_session} because $!\n";
172              
173 0           foreach my $token (grep ! /^\.+$/, readdir DIR) {
174              
175 0 0 0       if ((-f "$plg->{dir_session}/$token/control/lastaccess") && (-f "$plg->{dir_session}/$token/control/username") && (-f "$plg->{dir_session}/$token/control/groups")) {
      0        
176 0           my $lastaccess = ${ Storable::retrieve "$plg->{dir_session}/$token/control/lastaccess" };
  0            
177              
178 0 0         if (time - $lastaccess > $plg->Session_timeout) {
179 0           print STDOUT "Delete expired session: $token\n";
180 0           system $plg->rm, '-rf', "$plg->{dir_session}/$token"
181             }
182             else {
183 0           $TokenDB{$token}->{data} = {};
184 0           @{$TokenDB{$token}->{control}}{qw/lastaccess username groups/} = ($lastaccess, ${Storable::retrieve "$plg->{dir_session}/$token/control/username"}, ${Storable::retrieve "$plg->{dir_session}/$token/control/groups"});
  0            
  0            
  0            
185              
186 0 0         opendir __TOKEN, "$plg->{dir_session}/$token/data" or die "Could not read session directory $plg->{dir_session}/$token/data because $!\n";
187              
188 0           foreach my $record (grep ! /^\.{1,2}$/, readdir __TOKEN) {
189 0 0         next unless -f "$plg->{dir_session}/$token/data/$record";
190 0           $record = Encode::decode('utf8', $record);
191 0           $TokenDB{$token}->{data}->{$record} = Storable::retrieve "$plg->{dir_session}/$token/data/$record";
192 0 0         $TokenDB{$token}->{data}->{$record} = ${ $TokenDB{$token}->{data}->{$record} } if 'SCALAR' eq ref $TokenDB{$token}->{data}->{$record}
  0            
193             }
194              
195 0           close __TOKEN;
196 0           print STDOUT "Restore session : $token (". scalar(keys %{$TokenDB{$token}->{data}}) ." records)\n"
  0            
197             }
198             }
199             else {
200 0           print STDOUT "Delete corrupt session: $token\n";
201 0           system $plg->rm,'-rf',"$plg->{dir_session}/$token"
202             }
203             }
204              
205 0           closedir DIR;
206 0           print STDOUT "\n";
207              
208              
209             #print STDERR Dumper( $app ) ;exit;
210             #print STDERR Dumper( $plg->config->{Routes} ) ;exit;
211             #print STDERR Dumper( $plg->auth_config ) ;exit;
212             #print STDERR Dumper \%TokenDB; exit;
213             #print STDERR "---------\n*". $plg->dir_session ."*\n---------\n";
214              
215             ## Catch hard errors
216             # $app->add_hook(
217             # Dancer2::Core::Hook->new( name => 'init_error', code => sub
218             # {
219             # print STDERR "\n---------\n";
220             # print STDERR "debug : ". Dumper( $_[0] );
221             # print STDERR "\n---------\n";
222             #
223             # $plg->error( 'Unknown route '. $plg->dsl->request->env->{REQUEST_URI} );
224             # $_[0]->{content} = "{ \"error\" : \"". $plg->error . "\", \"reply\" : {} }"
225             # }
226             # )
227             # );
228              
229              
230             # Hook, BEFORE the main app process the request
231              
232             $app->add_hook( Dancer2::Core::Hook->new( name => 'before', code => sub
233             {
234 0     0     $plg->error(0);
235 0           $plg->token(undef);
236 0           $plg->data({});
237 0 0         $plg->sort( exists $app->request->query_parameters->{sort} ? $app->request->query_parameters->{sort} =~/(?i)1|t|y/ ? 1:0:0); # sort default is 0
    0          
238 0 0         $plg->pretty( exists $app->request->query_parameters->{pretty} ? $app->request->query_parameters->{pretty}=~/(?i)1|t|y/ ? 1:0:1); # pretty default is 1
    0          
239 0   0       $plg->ClientIP($app->request->env->{HTTP_X_REAL_IP} // $app->request->address // '127.0.0.1'); # Client IP address, even if running from a reverse proxy
      0        
240              
241             # format
242 0           foreach (qw/from to/) {
243              
244 0 0         if (exists $app->request->query_parameters->{$_}) {
245              
246 0 0         if ( $app->request->query_parameters->{$_} =~ $fmt_rgx ) {
247 0           $plg->Format->{$_} = $app->request->query_parameters->{$_}
248             }
249             else {
250              
251 0 0         if ( $app->request->query_parameters->{$_} eq 'jsn' ) { $plg->Format->{$_} = 'json' }
  0 0          
    0          
    0          
252 0           elsif ( $app->request->query_parameters->{$_} eq 'yml' ) { $plg->Format->{$_} = 'yaml' }
253 0           elsif ( $app->request->query_parameters->{$_} eq 'txt' ) { $plg->Format->{$_} = 'human'}
254 0           elsif ( $app->request->query_parameters->{$_} eq 'text') { $plg->Format->{$_} = 'human'}
255             else {
256 0           $plg->Format->{to} = $plg->config->{'Default format'};
257 0           $plg->error("Format parameter $_ ( ".$app->request->query_parameters->{$_}.' ) is not one of the : '. join(', ',keys %Formats));
258 0           $plg->reply
259             }
260             }
261             }
262             else {
263 0           $plg->Format->{$_} = $plg->config->{'Default format'}
264             }
265             }
266              
267             # Header Content-Type
268 0           $app->request->header('Content-Type'=> $Formats{$plg->Format->{to}});
269              
270              
271             # Check client IP address against the access rules
272 0           $plg->error('Client IP address '.$plg->ClientIP.' is not allowed');
273 0           for (my $i=0; $i<@{$plg->rules_compiled}; $i++) {
  0            
274 0 0         if ( $plg->ClientIP =~ $plg->rules_compiled->[$i] ) {
275 0           $plg->error(0);
276             last
277 0           }
278             }
279 0 0         $plg->reply if $plg->error;
280              
281              
282             # route name
283 0 0         if ( $app->request->{route}->{regexp} =~/^\^[\/\\]+(.*?)[\/\\]+\(\?#token.*/ ) { $plg->route_name($1) }
  0 0          
284 0           elsif ( $app->request->{route}->{regexp} =~/^\^[\/\\]+(.*?)\$/ ) { $plg->route_name($1) }
285 0           else { $plg->error('Could not recognize the route'); $plg->reply }
  0            
286              
287 0 0         unless (exists $plg->config->{Routes}->{$1}) {
288 0           $_=$1; s/\\//g;
  0            
289 0           $plg->error("Unknown route $_ you have to add it at your config.yml under the Routes");
290 0           $plg->reply
291             }
292              
293             # The following code must pruduce the hash/array $plg->data from the posted text at Perl INTERNAL format
294 0 0         if ($app->request->body) {
295              
296 0           eval {
297              
298 0 0         if ('json' eq $plg->Format->{from}) { $JSON->utf8(1); $plg->data( $JSON->decode( $app->request->body ) ); $JSON->utf8(0) }
  0 0          
  0 0          
  0 0          
    0          
299 0           elsif ('yaml' eq $plg->Format->{from}) { $plg->data( $YAML->load( $app->request->body ) ) }
300 0           elsif ('xml' eq $plg->Format->{from}) { $plg->data( $XML->xml2hash( $app->request->body ) ) }
301 0           elsif ('perl' eq $plg->Format->{from}) { $plg->data( eval $app->request->body ) }
302 0           elsif ('human' eq $plg->Format->{from}) { my $ref={};
303              
304 0           foreach (split /\v+/, $app->request->body) {
305 0           my @array = split /\s*(?:=|\:|-->|->|\|)+\s*/, $_;
306 0 0         next unless @array;
307              
308 0 0         if ($#array==0) {
309 0           $ref->{data}->{default} = $array[0]
310             }
311             else {
312 0           $ref->{data}->{$array[0]} = join ',', @array[1 .. $#array]
313             }
314             }
315              
316 0           $plg->data($ref)
317             }
318             };
319              
320 0 0         if ($@) {
321 0           $@ =~s/[\s\v\h]+/ /g;
322 0           $plg->error('Data parsing as '.$plg->Format->{from}." failed because $@");
323 0           $plg->reply
324             }
325              
326             # This should croak for wide characters because of the intentional Perl INTERNAL format
327             #print STDERR "----------- in\n"; foreach (keys %{ $plg->data }) { print STDERR "$_ -> @{ $plg->data->{$_} }" } print STDERR "\n-----------\n"; # for json, yaml
328             #print STDERR "----------- in\n"; my $h=$plg->data->{root}; foreach (keys %{ $plg->data->{root} }) { print STDERR "$_ , $h->{$_}\n" } print STDERR "-----------\n"; # for the xml
329             }
330              
331              
332             # Define the token if sent as a query parameter
333 0 0         if (exists $app->request->query_parameters->{token}) {
334 0           $plg->token($app->request->query_parameters->{token});
335             delete $app->request->query_parameters->{token}
336 0           }
337              
338             # Delete not needed control url parameters
339 0           foreach (qw/from to sort pretty message/) {
340 0           delete $app->request->query_parameters->{$_}
341             }
342              
343 0 0         if ('HASH' eq ref $plg->data) {
    0          
344              
345             # Use data token as ... token !
346 0 0 0       if ((exists $plg->data->{token}) && (! defined $plg->token)) {
347 0           $plg->token($plg->data->{token});
348             delete $plg->data->{token}
349 0           }
350              
351             # Use the url parameters as data
352 0           foreach (keys %{$app->request->query_parameters}) {
  0            
353 0           $plg->data->{$_} = $app->request->query_parameters->{$_}
354             }
355             }
356             elsif ('ARRAY' eq ref $plg->data) {
357             # probably we will should push the query parameters to data list or something else fancy
358             # so far yada yada
359             }
360             else {
361 0           $plg->error('Posted data are not keys or list'); $plg->reply
  0            
362             }
363              
364 0           }));
365              
366              
367             # Hook ONLY for the protected routes, before the main app do anything
368             # halt if the session is expired, otherelse update the lastaccess
369              
370             $app->add_hook( Dancer2::Core::Hook->new(name=>'before', code=>sub{
371 0 0 0 0     return unless (exists $plg->config->{Routes}->{$plg->route_name}) && ($plg->config->{Routes}->{$plg->route_name}->{Protected} == 1);
372              
373 0 0         if (! defined $plg->token ) { $plg->error("You must provide a token to use the protected route $plg->{route_name}"); $plg->reply }
  0            
  0            
374 0 0         if (! exists $TokenDB{ $plg->token } ) { $plg->error('Invalid token'); $plg->reply }
  0            
  0            
375 0           $dir = $plg->dir_session.'/'.$plg->token;
376              
377 0 0         if (time - $TokenDB{ $plg->token }->{control}->{lastaccess} > $plg->Session_timeout) {
378 0           $plg->error('Session expired because its idle time '.(time - $TokenDB{ $plg->token }->{control}->{lastaccess}).' secs is more than the allowed '.$plg->Session_timeout.' secs');
379 0           system $plg->rm,'-rf',$dir;
380 0           delete $TokenDB{ $plg->token };
381 0           $plg->data({}); # clear user data
382 0           $plg->reply
383             }
384             else {
385             # update the lastaccess
386 0           $TokenDB{ $plg->token }->{control}->{lastaccess} = time;
387 0           Storable::lock_store \$TokenDB{ $plg->token }->{control}->{lastaccess}, "$dir/control/lastaccess"
388             }
389              
390             # Check if the user is member to all the Groups of the route
391 0           $tmp=0;
392              
393 0           foreach (@{$plg->config->{Routes}->{$plg->route_name}->{Groups}}) {
  0            
394              
395 0 0         if (exists $TokenDB{ $plg->token }->{control}->{groups}->{$_} ) {
396 0           $tmp=1;
397             last
398 0           }
399              
400 0 0         unless ($tmp) {
401 0           $plg->error('Required route groups are '. join(',',@{$plg->config->{Routes}->{$plg->route_name}->{Groups}}) .' your groups are '. join(',', sort keys %{$TokenDB{ $plg->token }->{control}->{groups}}));
  0            
  0            
402 0           $plg->reply
403             }
404             }
405              
406 0           }));
407              
408              
409             # Built-in route /WebService list the routes
410             $app->add_route(
411             regexp => '/WebService',
412             method => 'get',
413             code => sub {
414              
415             $plg->reply(
416             {
417             Application => $app->{name},
418             Server => { bind => $app->request->env->{SERVER_NAME} , port => $app->request->env->{SERVER_PORT} , uptime => time - $^T },
419             'Login idle timeout' => $plg->Session_timeout,
420             'Auth method' => ( $plg->auth_method ? $plg->auth_method :'UNDEFINED' ),
421             Version => {
422 0 0   0     $app->{name} => ( exists $plg->dsl->config->{appversion} ? $plg->dsl->config->{appversion} : '0.0.0' ),
    0          
423             Dancer2 => $Dancer2::VERSION,
424             Os => $plg->OS,
425             Perl => $],
426             WebService => $VERSION
427             }
428             }
429             )
430             }
431 0           );
432              
433             # Built-in route /WebService/:what
434             $app->add_route(
435             regexp => '/WebService/:what?',
436             method => 'get',
437 0     0     code => sub { $plg->error(0);
438              
439 0 0         if ( $app->request->param('what') =~/(?i)\Ar/ ) {
    0          
440              
441             $plg->reply(
442             {
443             'Built in' => {
444 0           'Protected' => [ map { $_ } grep $plg->config->{Routes}->{$_}->{'Built in'} && $plg->config->{Routes}->{$_}->{Protected}==1, sort keys %{$plg->config->{Routes}} ],
  0            
445 0           'Public' => [ map { $_ } grep $plg->config->{Routes}->{$_}->{'Built in'} && $plg->config->{Routes}->{$_}->{Protected}==0, sort keys %{$plg->config->{Routes}} ]
  0            
446             },
447             $plg->dsl->config->{appname} => {
448 0           'Protected' => [ map { s/\\//g; $_ } grep ! $plg->config->{Routes}->{$_}->{'Built in'} && $plg->config->{Routes}->{$_}->{Protected}==1, sort keys %{$plg->config->{Routes}} ],
  0            
  0            
449 0   0       'Public' => [ map { s/\\//g; $_ } grep ! $plg->config->{Routes}->{$_}->{'Built in'} && $plg->config->{Routes}->{$_}->{Protected}==0, sort keys %{$plg->config->{Routes}} ]
  0   0        
  0   0        
  0   0        
450             }
451             }
452             )
453             }
454             elsif ( $app->request->param('what') =~/(?i)\Ac/ ) {
455              
456             $plg->reply(
457             {
458             Address => $plg->ClientIP,
459             Port => $app->request->env->{REMOTE_PORT},
460 0           Agent => $app->request->agent,
461             Protocol => $app->request->protocol,
462             'Is secure' => $app->request->secure,
463             'Http method' => $app->request->method,
464             'Header accept' => $app->request->header('accept'),
465             'Parameters url' => join(' ', $app->request->params('query')),
466             'Parameters route'=> join(' ', $app->request->params('route')),
467             'Parameters body' => join(' ', $app->request->params('body'))
468             }
469             )
470             }
471             else {
472 0           $plg->error('Not existing internal route /WebService/'.$app->request->param('what')); $plg->reply
  0            
473             }
474             }
475 0           );
476              
477             # logout and delete the session
478             $app->add_route(
479             regexp => '/logout',
480             method => $_,
481             code => sub {
482 0     0     $plg->error(0);
483 0           delete $TokenDB{ $plg->token };
484 0 0         system $plg->rm,'-rf', $plg->dir_session.'/'.$plg->token if -d $plg->dir_session.'/'.$plg->token;
485 0           $plg->data({});
486 0           $plg->reply( { token => $plg->token } )
487             }
488 0           ) foreach 'get','post','put';
489              
490              
491             # Authentication
492             $app->add_route(
493             regexp => '/login',
494             method => $_,
495             code => sub {
496 0 0   0     if ($plg->auth_method eq '') { $plg->error('There is not any enabled authentication method at the config.yml'); $plg->reply }
  0            
  0            
497              
498             # Check the input parameters
499 0 0         foreach ('username','password') {unless (exists $plg->data->{$_}) { $plg->error("Missing mandatory key $_"); $plg->reply }}
  0            
  0            
  0            
500 0 0         if ( $plg->data->{username} =~/^\s*$/ ) { $plg->error('username can not be blank'); $plg->reply }
  0            
  0            
501 0 0         if ( $plg->data->{password} eq '' ) { $plg->error('password can not be blank'); $plg->reply }
  0            
  0            
502              
503 0           my $app = shift;
504 0           my $groups = {};
505 0           $plg->error('authorization error');
506            
507             # Internal
508 0 0         if ($plg->auth_method eq 'INTERNAL') {
509              
510 0 0         if (exists $plg->auth_config->{Accounts}->{ $plg->data->{username} }) {
511 0 0         if ($plg->auth_config->{Accounts}->{ $plg->data->{username} } eq '') {$plg->error(0)} # global password
  0 0          
512 0           elsif ($plg->auth_config->{Accounts}->{ $plg->data->{username} } eq $plg->data->{password}) {$plg->error(0)} # normal
513             }
514              
515 0 0 0       if ($plg->error && exists $plg->auth_config->{Accounts}->{''}) {
516 0 0         if ($plg->auth_config->{Accounts}->{''} eq '') {$plg->error(0)} # global user and global password
  0 0          
517 0           elsif ($plg->auth_config->{Accounts}->{''} eq $plg->data->{password}) {$plg->error(0)} # global user and normal password
518             }
519             }
520              
521             # The external authorization scripts expect at least the two arguments
522             #
523             # 1) username as hex string (for avoiding shell attacks)
524             # 2) password as hex string
525             #
526             # Script output must be the two lines
527             #
528             # 1) 0 for successful login , or the error message at fail
529             # 2) All the groups that the user belongs
530              
531             else {
532 0           my @output;
533 0           my $command = $plg->auth_command.' '.unpack('H*', $plg->data->{username}).' '.unpack('H*', $plg->data->{password});
534 0 0         if (@{$plg->auth_config->{Arguments}}) { $command .=' '.join ' ', map { "\"$_\"" } @{$plg->auth_config->{Arguments}} }
  0            
  0            
  0            
  0            
535              
536             # Execute the external authorization utility and capture its 3 lines output at @output array
537 0 0         open SHELL, '-|', "$command 2> /dev/null" or die "Could run AuthScript \"$command\" because \"$?\"\n";
538 0           while() {s/^\s*(.*?)\s*$/$1/; push @output,$_}
  0            
  0            
539 0           close SHELL;
540              
541 0 0         unless (2 == scalar @output) { $plg->error('Expected 2 lines output instead of '.scalar(@output).' at auth method '.$plg->auth_method ); $plg->reply }
  0            
  0            
542 0           $plg->error($output[0]);
543 0           map { $groups->{$_} = 1 } split /,/,$output[1]
  0            
544             }
545              
546 0 0         $plg->reply if $plg->error;
547              
548             # Create the token and session dir
549 0 0         open URANDOM__, '<', '/dev/urandom' or die "\nCould not read device /dev/urandom\n";
550 0           read URANDOM__, my $i, 12;
551 0           close URANDOM__;
552 0           $tmp = time.'-'.unpack 'h*',$i;
553 0           $i=0;
554 0           while ( -e $plg->dir_session .'/'. $tmp .'-'. $i++ ) {}
555 0           $tmp .= '-'. (--$i);
556              
557 0           foreach ("$plg->{dir_session}/$tmp", "$plg->{dir_session}/$tmp/control", "$plg->{dir_session}/$tmp/data") {
558 0 0         unless (mkdir $_) { $plg->error("Could not create session directory $_ because $!"); $plg->reply }
  0            
  0            
559             }
560              
561 0           $TokenDB{$tmp}->{data} = {};
562 0           @{$TokenDB{$tmp}->{control}}{qw/lastaccess groups username/} = (time,$groups,$plg->data->{username});
  0            
563              
564 0           while (my ($k,$v) = each %{ $TokenDB{$tmp}->{control} }) {
  0            
565              
566 0 0         unless ( Storable::lock_store \$v, "$plg->{dir_session}/$tmp/control/$k" ) {
567 0           $plg->error("Could not store session data $_[$i] because $!"); $plg->reply
  0            
568             }
569             }
570              
571 0           $plg->reply( { token=>$tmp, groups=>[sort keys %{$groups}] } )
  0            
572 0           }) foreach 'post', 'put'
573             }
574              
575              
576              
577             # Accepts a Perl data structure, and under the key "reply" returns a string formated as : json, xml, yaml, perl or human
578             # It also returns any error defined from the Error(...)
579             # A typical response is
580             #
581             # {
582             # "reply" : { "k1" : "B", "k2" : "v2" },
583             # "error" : "oh no"
584             # }
585             #
586             # reply
587             # reply( 'hello world' )
588             # reply( [ 'a', 'b' , 'c' ] )
589             # reply( { k1=>'v1', k1=>'v1' } )
590             # reply( 'a', 'b' , 'c' )
591             # reply( \&SomeFunction )
592              
593             sub reply :PluginKeyword
594             {
595 0     0 1   my $plg=shift;
596              
597 0 0         if ($#_ == -1) {
    0          
598 0           $plg->__STRUCTURE_TO_STRING( { error => $plg->error, reply => {} } ) # if no argument return only the error
599             }
600             elsif ($#_ == 0) {
601 0 0         if (ref $_[0]) {
602 0 0         if ('HASH' eq ref $_[0]) { $plg->__STRUCTURE_TO_STRING( { error => $plg->error, reply => $_[0] } ) }
  0 0          
    0          
    0          
    0          
603 0           elsif ('ARRAY' eq ref $_[0]) { $plg->__STRUCTURE_TO_STRING( { error => $plg->error, reply => $_[0] } ) }
604 0           elsif ('SCALAR' eq ref $_[0]) { $plg->__STRUCTURE_TO_STRING( { error => $plg->error, reply => ${$_[0]} } ) }
  0            
605 0           elsif ('GLOB' eq ref $_[0]) { $plg->__STRUCTURE_TO_STRING( { error => $plg->error, reply => 'GLOB' } ) }
606             elsif ('CODE' eq ref $_[0]) {
607 0           @keys = &{$_[0]}();
  0            
608              
609 0 0         if (0 == $#keys) {
610 0 0         if (ref $keys[0]) {
611 0 0         if ('HASH' eq ref $keys[0]) { $plg->__STRUCTURE_TO_STRING( { error => $plg->error, reply => $keys[0] } ) }
  0 0          
    0          
    0          
    0          
612 0           elsif ('ARRAY' eq ref $keys[0]) { $plg->__STRUCTURE_TO_STRING( { error => $plg->error, reply => $keys[0] } ) }
613 0           elsif ('SCALAR' eq ref $keys[0]) { $plg->__STRUCTURE_TO_STRING( { error => $plg->error, reply => ${$keys[0]} } ) }
  0            
614 0           elsif ('GLOB' eq ref $keys[0]) { $plg->__STRUCTURE_TO_STRING( { error => $plg->error, reply => 'GLOB' } ) }
615 0           elsif ('CODE' eq ref $keys[0]) { $plg->__STRUCTURE_TO_STRING( { error => $plg->error, reply => 'CODE' } ) }
616 0           else { $plg->__STRUCTURE_TO_STRING( { error => $plg->error, reply => ${$keys[0]} } ) }
  0            
617             }
618             else {
619 0           $plg->__STRUCTURE_TO_STRING( { error => $plg->error, reply => $keys[0] } )
620             }
621             }
622             else {
623 0           $plg->__STRUCTURE_TO_STRING( { error => $plg->error, reply => [ @keys ] } )
624             }
625             }
626             }
627             else {
628 0           $plg->__STRUCTURE_TO_STRING( { error=> $plg->error, reply => $_[0] } )
629             }
630             }
631             else {
632 0           $plg->__STRUCTURE_TO_STRING( { error => $plg->error, reply => [ @_ ] } )
633             }
634              
635 0           $plg->dsl->halt( $plg->reply_text )
636 1     1   15 }
  1         3  
  1         38  
637              
638              
639              
640             # Convert a hash, array, scalar reference to sting as $plg->reply_text
641             # The $_[0] is array/hash encoded to INTERNAL perl format
642             # $plg->__STRUCTURE_TO_STRING( Hash ref|Array ref|... )
643              
644             sub __STRUCTURE_TO_STRING
645             {
646 0     0     my $plg=shift;
647 0           $plg->reply_text('');
648              
649 0           eval {
650              
651 0 0         if ($plg->Format->{to} eq 'json') {
    0          
    0          
    0          
    0          
652 0           $JSON->canonical($plg->sort);
653              
654 0 0         if ($plg->pretty) {
655 0           $JSON->pretty(1); $JSON->space_after(1) } else {
  0            
656 0           $JSON->pretty(0); $JSON->space_after(0)
  0            
657             }
658              
659             #print STDERR "----------- out\n"; foreach (keys %{$_[0]->{reply}}) { print STDERR "$_ -> @{$_[0]->{reply}->{$_}} \n" } print STDERR "-----------\n";
660              
661 0 0 0       if (($plg->Format->{from} eq 'human') || ($plg->Format->{from} eq 'perl')) {
662 0           $JSON->utf8(0);
663 0           $plg->{reply_text} = Encode::decode('utf8', $JSON->encode($_[0]) );
664 0           $JSON->utf8(1)
665             }
666             else {
667 0           $JSON->utf8(0);
668 0           $plg->{reply_text} = $JSON->encode($_[0]);
669 0           $JSON->utf8(1)
670             }
671             }
672              
673              
674             elsif ($plg->Format->{to} eq 'xml') {
675             #print STDERR "----------- out\n"; print STDERR $plg->{reply_text} = $XML->hash2xml($_[0], utf8=>0, canonical=> $plg->sort, indent=> ($plg->pretty ? 2:0) ); ;print STDERR "\n-----------\n";
676 0 0         $plg->{reply_text} = $XML->hash2xml($_[0], canonical=> $plg->sort, indent=> ($plg->pretty ? 2:0) ) # $XML->hash2xml($_[0], utf8=>1, canonical=> $plg->sort, indent=> ($plg->pretty ? 2:0) )
677             }
678              
679             elsif ($plg->Format->{to} eq 'yaml') {
680             #print STDERR "----------- out\n"; print STDERR $YAML->dump($_[0]) ;print STDERR "\n-----------\n";
681 0           $plg->{reply_text} = $YAML->dump($_[0]); # It needs INTERNAL format
682             }
683              
684             elsif ($plg->Format->{to} eq 'human') {
685              
686 0 0 0       if (($plg->Format->{from} eq 'human') || ($plg->Format->{from} eq 'perl')) {
687 0     0     $Handler{WALKER}->($_[0], sub {my $val=shift; $val =~s/^\s*(.*?)\s*$/$1/; $plg->{reply_text} .= Encode::decode('utf8', join('.', @_) ." = $val\n" ) } )
  0            
  0            
688 0           }
689             else {
690 0     0     $Handler{WALKER}->($_[0], sub {my $val=shift; $val =~s/^\s*(.*?)\s*$/$1/; $plg->{reply_text} .= join('.', @_) ." = $val\n"})
  0            
  0            
691 0           }
692             }
693              
694             elsif ($plg->Format->{to} eq 'perl') {
695 0           $Data::Dumper::Indent=$plg->pretty;
696 0           $Data::Dumper::Sortkeys=$plg->sort;
697             # print STDERR "-----------\n"; foreach (keys %{$_[0]->{reply}}) { print STDERR "out : $_ -> @{$_[0]->{reply}->{$_}} \n" } print STDERR "\n-----------\n";
698              
699 0 0 0       if (($plg->Format->{from} eq 'human') || ($plg->Format->{from} eq 'perl')) {
700 0           $plg->{reply_text} = Encode::decode('utf8', Data::Dumper::Dumper $_[0])
701             }
702             else {
703 0           $plg->{reply_text} = Data::Dumper::Dumper $_[0]
704             }
705             }
706             };
707              
708 0 0         if ($@) {
709 0           $@=~s/[\v\h]+/ /g;
710 0           $plg->dsl->halt("{\"error\" : \"FATAL, Internal structure to string convertion failed\"}")
711             }
712             }
713              
714              
715              
716             # Retruns a hash referense if the data are posted as hash
717             # Retruns an array referense if the data are posted as list
718             #
719             # UserData(); # all posted data of $plg->data
720             # UserData( 'k1', 'k2' ); # only the selected posted data of $plg->data
721             #
722             sub UserData :PluginKeyword
723             {
724 0     0 1   my $plg=shift;
725              
726 0 0         if (@_) {
727              
728 0 0         if ('HASH' eq ref $plg->data) {
    0          
    0          
729 0           $tmp={}; @{$tmp}{@_}=();
  0            
  0            
730              
731 0           foreach (keys %{$plg->data}) {
  0            
732 0 0         delete $plg->data->{$_} unless exists $tmp->{$_}
733             }
734             }
735              
736             elsif ('ARRAY' eq ref $plg->data) {
737 0           $tmp={}; @{$tmp}{@_}=();
  0            
  0            
738 0           $plg->data( [ grep exists $tmp->{$_}, @{$plg->data} ] ) # Redefine the $plg->data from any valid values of the $plg->data
  0            
739             }
740              
741 0 0         elsif ('SCALAR' eq ref $plg->data) { foreach (@_) { $plg->data($_) if $_ eq ${$plg->data} } }
  0            
  0            
742 0 0         else { foreach (@_) { $plg->data($_) if $_ eq $plg->data } }
  0            
743             }
744              
745             $plg->data
746 1     1   2042 }
  1         2  
  1         5  
  0            
747              
748              
749             # Set session data
750             # Session data are not volatile like the posted by user
751             # They are persistent between requests until the user logout or its session get expired
752             # Returns a list of the stored keys
753             #
754             # SessionSet( k1 => 'v1', k2 => 'v2' );
755             # SessionSet( {k1 => 'v1', k2 => 'v2'} );
756              
757             sub SessionSet :PluginKeyword
758             {
759 0     0 1   my $plg=shift;
760              
761 0 0         if ($plg->session_enable) {
762              
763 0 0         if (defined $plg->token) {
764            
765 0 0         if ( ! exists $TokenDB{ $plg->token } ) {
766 0           $plg->error('Invalid token');
767 0           $plg->reply
768             }
769             }
770             else {
771 0           $plg->error('You need a token via login route for saving session data');
772 0           $plg->reply
773             }
774             }
775             else {
776 0           $plg->error('Sessions are disabled at application config.yml');
777 0           $plg->reply
778             }
779              
780 0 0 0       @_ = %{$_[0]} if (1 == @_) && ('HASH' eq ref $_[0]);
  0            
781 0           @keys=();
782              
783             # $_[$k] is the key
784             # $_[$v] is the value
785 0           for (my ($k,$v)=(0,1); $k<$#_-(@_ % 2); $k+=2,$v+=2) {
786 0 0         next if 'token' eq $_[$k];
787 0           push @keys, $_[$k];
788 0           $TokenDB{$plg->token}->{data}->{$_[$k]} = $_[$v];
789              
790 0 0         unless ( Storable::lock_store ref $_[$v] ? $_[$v] : \$_[$v], "$plg->{dir_session}/". $plg->token ."/data/$_[$k]" ) {
    0          
791 0           $plg->error("Could not store session key $_[$k] because $!");
792 0           $plg->reply
793             }
794             }
795              
796             @keys
797 1     1   927 }
  1         1  
  1         6  
  0            
798              
799              
800             # Retrieves session keys
801             #
802             # my %data = SessionGet(); # hash of all keys
803             # my %data = SessionGet('k1', 'k2', ...); # hash of the selected keys
804              
805             sub SessionGet :PluginKeyword
806             {
807 0     0 1   my $plg = shift;
808              
809 0 0         if ($plg->session_enable) {
810              
811 0 0         if (defined $plg->token) {
812              
813 0 0         if (! exists $TokenDB{$plg->token}) {
814 0           $plg->error('Invalid token');
815 0           $plg->reply
816             }
817             }
818             else {
819 0           $plg->error('You need a token via login route for reading session data');
820 0           $plg->reply
821             }
822             }
823             else {
824 0           $plg->error('Sessions are disabled at application config.yml');
825 0           $plg->reply
826             }
827              
828 0 0         if (0 == scalar @_) {
    0          
829             # all records
830 0           map { $_ , $TokenDB{$plg->token}->{data}->{$_}} keys %{$TokenDB{$plg->token}->{data}}
  0            
  0            
831             }
832             elsif ((1 == scalar @_)) {
833             # one record
834              
835 0 0         if ('ARRAY' eq ref $_[0]) {
836             # At new Perl versions hash slice %{$TokenDB{ $plg->token }->{data}}{@{$_[0]}}
837 0 0         map {exists $TokenDB{$plg->token}->{data}->{$_} ? ( $_ , $TokenDB{$plg->token}->{data}->{$_} ) : ()} @{$_[0]}
  0            
  0            
838             }
839             else {
840 0 0         exists $TokenDB{$plg->token}->{data}->{$_[0]} ? ( $_[0] , $TokenDB{$plg->token}->{data}->{$_[0]} ) : ()
841             }
842             }
843             else {
844             # Some records, normal, not array reference
845 0           map { ( $_ , $TokenDB{$plg->token}->{data}->{$_} ) } grep exists $TokenDB{$plg->token}->{data}->{$_} , @_
  0            
846             }
847 1     1   866 }
  1         2  
  1         4  
848              
849              
850             # Delete session data
851             # Retun a list of the deleted keys
852             #
853             # SessionDel() # delete all records
854             # SessionDel( 'k1', 'k2' ) # delete some records
855             # SessionDel( [ 'k1', 'k2' ] ) # delete some records
856              
857             #
858             sub SessionDel :PluginKeyword
859             {
860 0     0 1   my $plg = shift;
861              
862 0 0         if ($plg->session_enable) {
863              
864 0 0         if (defined $plg->token) {
865              
866 0 0         if ( ! exists $TokenDB{$plg->token} ) {
867 0           $plg->error('Invalid token');
868 0           $plg->reply
869             }
870             }
871             else {
872 0           $plg->error('You need a token via login route for deleting session data');
873 0           $plg->reply
874             }
875             }
876             else {
877 0           $plg->error('Sessions are disabled at application config.yml');
878 0           $plg->reply
879             }
880              
881 0           $dir = $plg->dir_session.'/'.$plg->token;
882 0           @keys=();
883              
884 0 0         if (@_) {
885 0 0 0       @_ = @{$_[0]} if (1 == @_) && ('ARRAY' eq ref $_[0]);
  0            
886              
887 0           foreach (@_) {
888              
889 0 0         if (exists $TokenDB{$plg->token}->{data}->{$_}) {
890 0           delete $TokenDB{$plg->token}->{data}->{$_};
891 0           push @keys, $_;
892 0 0         unlink "$dir/data/$_" if -f "$dir/data/$_"
893             }
894             }
895             }
896             else {
897              
898 0           foreach (keys %{$TokenDB{$plg->token}->{data}}) {
  0            
899 0           delete $TokenDB{$plg->token}->{data}->{$_};
900 0           push @keys, $_;
901 0 0         unlink "$dir/data/$_" if -f "$dir/data/$_"
902             }
903             }
904              
905             @keys
906 1     1   1108 }
  1         2  
  1         11  
  0            
907              
908              
909             # Set the error
910             # any['get','post','put'] => '/error1' => sub { Error('ok'); reply 'hello' }; # { "error" : "ok" , ... }
911             # any['get','post','put'] => '/error2' => sub { Error('ok'); reply }; # { "error" : "ok", reply: {} }
912             # any['get','post','put'] => '/error2' => sub { reply 'hello' }; # { "error" : "Something went wrong", ... }
913              
914 1 0   1 1 338 sub Error :PluginKeyword { $_[0]->error( exists $_[1] ? $_[1] : 'Something went wrong' ) }
  1     0   2  
  1         3  
  0            
915              
916             1;
917              
918             =pod
919              
920             =encoding UTF-8
921              
922             =head1 NAME
923              
924             Dancer2::Plugin::WebService - Rest APIs with login, persistent data, multiple in/out formats, IP security, role based access
925              
926             =head1 VERSION
927              
928             version 4.8.8
929              
930             =head1 SYNOPSIS
931              
932             get '/my_keys' => sub { reply { 'k1'=>'v1' , 'k2'=>'v2' } };
933              
934             curl $url/my_keys
935              
936             =head1 DESCRIPTION
937              
938             Create REST APIs with login, logout, persistent session data, IP security, role based access.
939             Multiple input/output supported formats : json , xml , yaml, perl , human
940             Post your data and keys as url parameters or content body text
941              
942             curl -X GET "$url/SomeRoute?k1=v1&k2=v2&k3=v3"
943             curl -X POST $url/SomeRoute -d '{ "k1":"v1", "k2":"v2", "k3":"v3" }'
944              
945             =head1 NAME
946              
947             Convert your functions to REST api with minimal effort
948              
949             =head1 URL parameters to format the reply
950              
951             You can use the B, B, B, B parameters to define the input/output format
952              
953             =over 2
954              
955             =item I , I
956              
957             Define the input/output format.
958              
959             You can define input/output formats independently.
960             B default is the B property B
961             Supported formats are
962              
963             json or jsn
964             yaml or yml
965             xml
966             perl
967             human or text or txt
968              
969             curl "$url/mirror?from=perl&to=xml" -d '{ "k1" => ["v1","v2","v3"] }'
970              
971             =item I
972              
973             If true the keys are returned sorted. The default is false because it is faster. Valid values are true, 1, yes, false, 0, no
974              
975             =item I
976              
977             If false, the data are returned as one line compacted. The default is true, for human readable output. Valid values are true, 1, yes, false, 0, no
978              
979             =back
980              
981             =head1 METHODS
982              
983             Plugin methods available for your main Dancer2 code
984              
985             =head2 UserData
986              
987             Get all or some of the posted data
988             Retruns a hash referense if the data are posted as hash
989             Retruns an array referense if the data are posted as list
990              
991             UserData Returns everything
992             UserData('k1','k2') Returns only the specific keys of the posted hash/list
993              
994             get '/SomePath' => sub { reply UserData };
995              
996             =head2 reply
997              
998             Your last route's statement. Accepts a Perl data structure, and return it as json, xml, yaml, perl or human under the key I
999              
1000             reply( 'hello world' )
1001             reply( \'hello world' )
1002             reply( 'a', 'b' , 'c' )
1003             reply( [ 'a', 'b' , 'c' ] )
1004             reply( { k1=>'v1', k2=>'v2' } )
1005             reply( &SomeFunction )
1006             reply( \&SomeFunction )
1007              
1008             =head2 Error
1009              
1010             Set the error. Normally at success B should be 0
1011             It does not stop the route execution. You must place it before the reply()
1012              
1013             get '/SomePath' => sub { Error('oups') ; reply };
1014             get '/SomePath' => sub { Error('ok') ; reply 'hello world' };
1015              
1016             =head2 SessionSet
1017              
1018             Store session persistent data, unlike the volatile common posted data. It is a protected method, I is required
1019              
1020             They are persistent between requests until they deleted, the user logout or their session get expired.
1021              
1022             You must pass your data as hash or hash reference.
1023              
1024             Returns a list of the stored keys.
1025              
1026             any['get','post'] => '/session_save' => sub
1027             {
1028             @arr = SessionSet( k1=>'v1' , k2=>'v2' );
1029             @arr = SessionSet( { k3=>'v3' , k4=>'v4' } );
1030             reply { 'Your saved keys are' => \@arr }
1031             };
1032              
1033             curl $url/session_save?token=17398-5c8a71b -H "$H" -X POST -d '{"k1":"v1", "k2":"v2", ... }'
1034              
1035             =head2 SessionGet
1036              
1037             Read session persistent data. It is a protected method, I
1038              
1039             Returns a hash
1040              
1041             any['post','put'] => '/session_read' => sub {
1042             my %hash1 = SessionGet( 'k1','k2' ); # some records
1043             my %hash2 = SessionGet( [ 'k1','k2' ] ); # some records
1044             my %hash3 = SessionGet(); # all records
1045             reply { %hash3 }
1046             };
1047              
1048             curl $url/session_read?token=17398-5c8a71b
1049              
1050             =head2 SessionDel
1051              
1052             Deletes session persistent data. It is a protected method, I
1053              
1054             Returns a list of the deleted keys
1055              
1056             SessionDel; delete all keys
1057             SessionDel( 'rec1', 'rec2', ... ); delete selected keys
1058             SessionDel( [ 'rec1', 'rec2', ... ] ); delete selected keys
1059              
1060             any['delete'] => '/session_delete' => sub {
1061             my $arg = UserData();
1062             my @arr = SessionDel( $arg );
1063             reply { 'Deleted keys' => \@arr }
1064             };
1065              
1066             curl -X DELETE $url/session_delete?token=17398-5c8a71b -H "$H" -d '["k1","k2","k9"]'
1067              
1068             {
1069             "error" : 0,
1070             "reply" : {
1071             "Deleted keys" : [ "k1" , "k2" ]
1072             }
1073             }
1074              
1075             =head1 Authentication and role based access control
1076              
1077             The routes can be either B or B
1078              
1079             =over 2
1080              
1081             =item B
1082              
1083             routes that you must provide the I, as returned by the I route.
1084             Afer login, you can save, update, read, delete persistent session data
1085              
1086             The B route is using the the first active authentication method of the I
1087              
1088             =item B
1089              
1090             routes that anyone can use without B , they do not support sessions / persistent data.
1091              
1092             =back
1093              
1094             =head1 Configuration file "I"
1095              
1096             This file customize the I, I, I, I and I. The following is an example
1097              
1098             appname : TestService
1099             appversion : 1.0.0
1100             environment : development
1101             layout : main
1102             charset : UTF-8
1103             template : template_toolkit
1104             engines : {template: {template_toolkit: {EVAL_PERL: 0, start_tag: '[%', end_tag: '%]' }}}
1105             plugins:
1106             WebService:
1107             Session enable : true
1108             Session directory : /var/lib/WebService
1109             Session idle timeout: 86400
1110             Default format : json
1111             Allowed hosts :
1112             - "127.*"
1113             - "172.20.20.*"
1114             - "????:????:????:6d00:20c:29ff:*:ffa3"
1115             - "10.*.?.*"
1116             - "*"
1117              
1118             Routes:
1119             text : { Protected: false }
1120             mirror : { Protected: false }
1121             Protected : { Protected: true }
1122             Protected_text_ref: { Protected: true }
1123             list : { Protected: false }
1124             list_ref : { Protected: false }
1125             hash : { Protected: false }
1126             code\/text : { Protected: false }
1127             code\/list : { Protected: false }
1128             code\/hash : { Protected: false }
1129             code\/text_ref : { Protected: false }
1130             code\/list_ref : { Protected: false }
1131             keys_selected : { Protected: false }
1132             git\/commit : { Protected: true, Groups: [ git , ansibleremote ] }
1133             session_save : { Protected: true, Groups: [] }
1134             session_read : { Protected: true, Groups: [] }
1135             session_delete : { Protected: true, Groups: [] }
1136              
1137             Authentication methods:
1138              
1139             - Name : INTERNAL
1140             Active : true
1141             Accounts :
1142             user1 : s3cr3T+PA55sW0rD
1143             user2 :
1144             : S3cREt-4-aLl
1145             # :
1146              
1147             - Name : Linux native users
1148             Active : false
1149             Command : MODULE_INSTALL_DIR/AuthScripts/Linux_native_authentication.sh
1150             Arguments : [ ]
1151             Use sudo : true
1152              
1153             - Name : Basic Apache auth for simple users
1154             Active : false
1155             Command : MODULE_INSTALL_DIR/AuthScripts/HttpBasic.sh
1156             Arguments : [ "/etc/htpasswd" ]
1157             Use sudo : false
1158              
1159             =head1 Authentication methods
1160              
1161             Authentication method can be INTERNAL or external executable Command.
1162              
1163             At INTERNAL you define the usernames / passwords directly at the I . The means any username or password,
1164             so if you want to allow all users to login no matter the username or the password use
1165              
1166             :
1167              
1168             This make sense if you just want to give anyone the ability for persistent data
1169              
1170             The protected routes, at config.yml have Protected:true and their required groups e.g. Groups:[grp1,grp2 ...]
1171              
1172             The user must be member to B defined groups
1173              
1174             If the route's Groups list is empty or missing, then the groups membership is ignored
1175              
1176             This way you can have user based access, because every user is allowed to access his assigned routes.
1177              
1178             =head1 Authentication scripts
1179              
1180             At production enviroments, probably you want to use external authenticators, accessed by plugable scripts e.g for the native "Linux native" authentication
1181              
1182             MODULE_INSTALL_DIR/AuthScripts/Linux_native_authentication.sh
1183              
1184             It is easy to write your own scripts for LDAP, Active Directory, OAuth 2.0, Keycload, etc external authenticators.
1185              
1186             If the script needs sudo, you must add the user running the application to sudoers e.g
1187              
1188             dendrodb ALL=(ALL:ALL) NOPASSWD: /usr/share/perl5/site_perl/Dancer2/Plugin/AuthScripts/some_auth_script.sh
1189              
1190             Please read the file AUTHENTICATION_SCRIPTS for the details
1191              
1192             =head1 IP access
1193              
1194             You can control which clients are allowed to use your application at the file I
1195              
1196             The rules are checked from up to bottom until there is a match. If no rule match then the client can not login. At rules your can use the wildcard characters * ?
1197              
1198             ...
1199             plugins:
1200             WebService:
1201             Allowed hosts:
1202             - 127.*
1203             - 10.*
1204             - 172.20.*
1205             - 32.??.34.4?
1206             - 4.?.?.??
1207             - ????:????:????:6d00:20c:29ff:*:ffa3
1208             - 192.168.0.153
1209             - "*"
1210              
1211             =head1 Sessions
1212              
1213             Upon successful login, the client is in session until logout or its session expired due to inactivity.
1214              
1215             While in session you can access protected routes and save, read, delete session persistent data.
1216              
1217             at the I You can change persistent data storage directory and session expiration
1218              
1219             =over 2
1220              
1221             =item B
1222              
1223             Be careful this directory must be writable from the user that is running the service
1224             To set the sessions directory
1225              
1226             plugins:
1227             WebService:
1228             Session directory : /var/lib/WebService
1229              
1230             or at your application
1231              
1232             setting('plugins')->{'WebService'}->{'Session directory'} = '/var/lib/WebService';
1233              
1234             =item B
1235              
1236             Sessions are expiring after some seconds of inactivity. You can change the amount of seconds either at the I
1237              
1238             plugins:
1239             WebService:
1240             Session idle timeout : 3600
1241              
1242             or at your application
1243              
1244             setting('plugins')->{'WebService'}->{'Session idle timeout'} = 3600;
1245              
1246             =back
1247              
1248             =head1 Built in plugin routes
1249              
1250             These are plugin built in routes
1251              
1252             WebService version
1253             WebService/client client propertis
1254             WebService/routes list the built-in and application routes
1255             login login
1256             logout logout
1257              
1258             Usage examples
1259              
1260             export url=http://127.0.0.1:3000 H="Content-Type: application/json"
1261             alias curl="$(/usr/bin/which curl) --silent --user-agent Perl"
1262              
1263             curl $url/WebService
1264             curl $url/WebService/client
1265             curl $url/WebService/routes?sort=true
1266             curl "$url/WebService?to=json&pretty=true&sort=true"
1267             curl $url/WebService?to=yaml
1268             curl "$url/WebService?to=xml&pretty=false"
1269             curl "$url/WebService?to=xml&pretty=true"
1270             curl $url/WebService?to=human
1271             curl $url/WebService?to=perl
1272             curl $url
1273              
1274             =head1 Application routes
1275              
1276             Based on the code of our TestService ( lib/TestService.pm ) some examples of how to login, logout, and route usage
1277              
1278             curl "$url/mirror?from=json&to=json&k1=a&k2=b" -d '{"k1" : ["one","two","three"]}'
1279             curl "$url/mirror?to=xml&pretty=true" -d '{"k1" : ["one","two","three"]}'
1280             curl "$url/mirror?from=yaml&to=perl" -d '"k1" : ["one","two","three"]'
1281             curl "$url/mirror?from=xml&to=yaml" -d 'onetwo'
1282              
1283             Login
1284              
1285             curl -X POST $url/login -H "$H" -d '{"username": "user1", "password": "s3cr3T+PA55sW0rD"}'
1286              
1287             Protected application routes
1288              
1289             curl $url/text
1290             curl $url/text?token=17393926-5c8-0
1291             curl $url/session_save?token=17393926-5c8-0 -H "$H" -X POST -d '{"k1":"v1", "k2":"v2", "k3":"v3"}'
1292             curl $url/session_read?token=17393926-5c8-0
1293             curl $url/session_delete?token=17393926-5c8-0 -H "$H" -X DELETE -d '["k3","k8","k9"]'
1294             curl $url/session_read?token=17393926-5c8-0
1295              
1296             Logout
1297              
1298             curl $url/logout?token=17393926-5c8-0
1299             curl $url/logout -d '{"token":"17393926-5c8-0"}' -H "$H" -X POST
1300              
1301             =head1 Plugin Installation
1302              
1303             You should your run your APIs as a non privileged user e.g. the "dancer"
1304              
1305             getent group dancer >/dev/null || groupadd dancer
1306             getent passwd dancer >/dev/null || useradd -g dancer -l -m -c "Dancer2 WebService" -s $(which nologin) dancer
1307             i=/var/lib/WebService; [ -d $i ] || { mkdir $i; chown -R dancer:dancer $i; }
1308             i=/var/log/WebService; [ -d $i ] || { mkdir $i; chown -R dancer:dancer $i; }
1309             cpanm Dancer2
1310             cpanm Dancer2::Plugin::WebService
1311              
1312             =head1 Create a sample application e.g. the "TestService"
1313              
1314             Follow the I document to create the sample application I
1315              
1316             =head1 Start the application
1317              
1318             To start it manual as user I from the command line
1319              
1320             =over 2
1321              
1322             =item Production
1323              
1324             sudo -u dancer plackup --host 0.0.0.0 --port 3000 --server Starman --workers=5 --env development -a /home/dancer/TestService/bin/app.psgi
1325              
1326             =item While developing
1327              
1328             sudo -u dancer plackup --host 0.0.0.0 --port 3000 --env development --app /home/dancer/TestService/bin/app.psgi --server HTTP::Server::PSGI
1329              
1330             =back
1331              
1332             view also the INSTALL document for details
1333              
1334             =head1 Configure the loggger at the environment file
1335              
1336             I
1337              
1338             log : "debug" # core, debug, info, warning, error
1339             show_stacktrace : 0
1340             no_server_tokens : 1
1341             warnings : 1 # should Dancer2 consider warnings as critical errors?
1342             show_errors : 1 # if true shows a detailed debug error page , otherse the views/404.tt or public/404.html
1343             startup_info : 1 # print the banner
1344             no_server_tokens : 1 # disable server tokens in production environments
1345             logger : "file" # console: to STDOUT , file:to file
1346             engines :
1347             logger :
1348             file :
1349             log_format : '{"ts":"%T","host":"%h","pid":"%P","message":"%m"}'
1350             log_dir : "/tmp"
1351             file_name : "test.log"
1352              
1353             =head1 See also
1354              
1355             B Route PSGI requests for RESTful web applications
1356              
1357             B A plugin for writing RESTful apps with Dancer2
1358              
1359             B Perl extension for writing pRPC servers
1360              
1361             B A simple, unified interface to XML-RPC and JSON-RPC
1362              
1363             B Pure Perl implementation for an XML-RPC client and server.
1364              
1365             B JSON RPC Server Implementation
1366              
1367             =head1 AUTHOR
1368              
1369             George Bouras
1370              
1371             =head1 COPYRIGHT AND LICENSE
1372              
1373             This software is copyright (c) 2026 by George Bouras.
1374              
1375             This is free software; you can redistribute it and/or modify it under
1376             the same terms as the Perl 5 programming language system itself.
1377              
1378             =cut
1379              
1380             __END__