File Coverage

blib/lib/Dancer2/Plugin/WebService.pm
Criterion Covered Total %
statement 42 416 10.1
branch 0 202 0.0
condition 0 58 0.0
subroutine 14 28 50.0
pod 5 6 83.3
total 61 710 8.5


line stmt bran cond sub pod time code
1             # ABSTRACT:RESTful Web Services 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.4.3';
9 1     1   54544 use strict;
  1         2  
  1         23  
10 1     1   4 use warnings;
  1         2  
  1         21  
11 1     1   506 use Encode;
  1         12663  
  1         64  
12 1     1   446 use Dancer2::Plugin;
  1         240614  
  1         10  
13 1     1   47089 use Storable;
  1         3  
  1         71  
14 1     1   607 use XML::Hash::XS; $XML::Hash::XS::canonical=0; $XML::Hash::XS::utf8=1; $XML::Hash::XS::doc=0; $XML::Hash::XS::root='root'; $XML::Hash::XS::encoding='utf-8'; $XML::Hash::XS::indent=2; $XML::Hash::XS::xml_decl=0;
  1         950  
  1         66  
15 1     1   557 use Data::Dumper; $Data::Dumper::Trailingcomma=0; $Data::Dumper::Indent=2; $Data::Dumper::Terse=1; $Data::Dumper::Deepcopy=1; $Data::Dumper::Purity=1; $Data::Dumper::Sortkeys=0;
  1         5249  
  1         85  
16 1     1   479 use YAML::Syck; $YAML::Syck::ImplicitTyping=1; $YAML::Syck::Headless=0; $YAML::Syck::ImplicitUnicode=0;
  1         1618  
  1         78  
17 1     1   834 use Cpanel::JSON::XS; my $JSON=Cpanel::JSON::XS->new; $JSON->space_before(1);$JSON->canonical(0); $JSON->allow_tags(1); $JSON->allow_unknown(0); $JSON->pretty(0); $JSON->indent(1); $JSON->space_after(1); $JSON->max_size(0); $JSON->relaxed(0); $JSON->shrink(0); $JSON->allow_nonref(0); $JSON->allow_blessed(1); $JSON->convert_blessed(1); $JSON->max_depth(1024); $JSON->utf8(0);
  1         1604  
  1         5267  
18              
19              
20             if ($^O=~/(?i)MSWin/) {warn "Operating system is not supported\n"; exit 1}
21             my $dir;
22             my %Handler;
23             my %TokenDB = ();
24             my %Formats = ( json=>'application/json', xml=>'text/xml', yaml=>'text/x-yaml', perl=>'text/html', human=>'text/html' );
25             $_ = join '|', sort keys %Formats;
26             my $fmt_rgx = qr/^($_)$/;
27              
28             has error => (is=>'rw', lazy=>1, default=> 0);
29             has sort => (is=>'rw', lazy=>1, default=> 0);
30             has pretty => (is=>'rw', lazy=>1, default=> 1);
31             has route_name => (is=>'rw', lazy=>1, default=> '');
32             has ClientIP => (is=>'rw', lazy=>1, default=> '');
33             has reply_text => (is=>'rw', lazy=>1, default=> '');
34             has auth_method => (is=>'rw', lazy=>1, default=> '');
35             has auth_command => (is=>'rw', lazy=>1, default=> '');
36             has auth_config => (is=>'rw', lazy=>1, default=> sub{ {} });
37             has data => (is=>'rw', lazy=>1, default=> sub{ {} }); # user posted data as hash
38             has Format => (is=>'rw', lazy=>1, default=> sub{ {from => undef, to => undef} });
39             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}} );
40             has rules => (is=>'ro', lazy=>0, from_config=>'Allowed hosts', default=> sub{ ['127.*', '192.168.*', '172.16.*'] });
41             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});
42             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} );
43             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});
44              
45              
46             # Recursive walker of custom Perl Data Structures
47             %Handler=(
48             SCALAR => sub { $Handler{WALKER}->(${$_[0]}, $_[1], @{$_[2]} )},
49             ARRAY => sub { $Handler{WALKER}->($_, $_[1], @{$_[2]}) for @{$_[0]} },
50             HASH => sub { $Handler{WALKER}->($_[0]->{$_}, $_[1], @{$_[2]}, $_) for sort keys %{$_[0]} },
51             '' => sub { $_[1]->($_[0], @{$_[2]}) },
52             WALKER => sub { my $data = shift; $Handler{ref $data}->($data, shift, \@_) }
53             );
54              
55              
56             sub BUILD
57             {
58 0     0 0   my $plg = shift;
59 0           my $app = $plg->app;
60              
61             # Security of the built-in routes
62 0           $plg->config->{Routes}->{WebService} = {Protected=>0};
63 0           $plg->config->{Routes}->{login} = {Protected=>0};
64 0           $plg->config->{Routes}->{logout} = {Protected=>1, Groups=>[]};
65              
66             # Default settings
67 0   0       $app->config->{charset} //= 'UTF-8';
68 0   0       $app->config->{encoding} //= 'UTF-8';
69 0   0       $app->config->{show_errors} //= 0;
70 0 0 0       $plg->config->{'Default format'} = 'json' if ((! exists $plg->config->{'Default format'}) || ($plg->config->{'Default format'} !~ $fmt_rgx));
71 0           $app->config->{content_type} = $Formats{ $plg->config->{'Default format'} };
72              
73             # Module directory
74 0           (my $module_dir =__FILE__) =~s|/[^/]+$||;
75 0 0         unless (-d $module_dir) {warn "Could not find the Dancer2::Plugin::WebService installation directory\n"; exit 1}
  0            
  0            
76              
77             # Use the first active authentication method
78              
79 0           foreach my $method (@{$plg->config->{'Authentication methods'}}) {
  0            
80 0 0 0       next unless ((exists $method->{Active}) && ($method->{Active}=~/(?i)[y1t]/));
81 0           $plg->auth_method( $method->{Name} );
82              
83             # If the Authorization method is an external script
84 0 0         if ($plg->auth_method ne 'INTERNAL') {
85 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            
86 0           $method->{Command} =~s/^MODULE_INSTALL_DIR/$module_dir/;
87 0 0         unless (-f $method->{Command}) {warn "Sorry, could not found the external authorization utility $method->{Command}\n"; exit 1}
  0            
  0            
88 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            
89              
90 0 0 0       if ((exists $method->{'Use sudo'}) && ($method->{'Use sudo'}=~/(?i)[y1t]/)) {
91 0           my $sudo = undef;
92 0 0 0       foreach (qw[/usr/bin /bin /usr/sbin /sbin]) { if ((-f "$_/sudo") && -x ("$_/sudo")) { $sudo="$_/sudo"; last } }
  0            
  0            
  0            
93 0 0         unless (defined $sudo) {warn "Could not found sudo command\n"; exit 1}
  0            
  0            
94 0           $plg->auth_command( "$sudo \Q$method->{Command}\E" )
95             }
96             else {
97 0           $plg->auth_command( "\Q$method->{Command}\E" )
98             }
99             }
100              
101 0           delete @{$method}{'Name','Active','Command','Use sudo'};
  0            
102 0   0       $method->{Arguments} //= [];
103 0           $plg->auth_config($method);
104             last
105 0           }
106              
107 0           delete $plg->config->{'Authentication methods'};
108              
109             # Check the active auth method if there are protected routes
110 0           foreach (keys %{$plg->config->{Routes}}) {
  0            
111              
112 0 0 0       if ((exists $plg->config->{Routes}->{$_}->{Protected}) && ($plg->config->{Routes}->{$_}->{Protected}=~/(?i)[y1t]/)) {
113 0           $plg->config->{Routes}->{$_}->{Protected} = 1;
114              
115 0 0         if ($plg->auth_method eq '') {
116 0           warn "While there is at least one protected route ( $_ ) there is not any active authorization method\n"; exit 1
  0            
117             }
118             else {
119              
120 0 0         if (exists $plg->config->{Routes}->{$_}->{Groups}) {
121             $plg->config->{Routes}->{$_}->{Groups} = [ $plg->config->{Routes}->{$_}->{Groups} ] unless 'ARRAY' eq ref $plg->config->{Routes}->{$_}->{Groups}
122 0 0         }
123             else {
124 0           $plg->config->{Routes}->{$_}->{Groups} = []
125             }
126             }
127             }
128             else {
129 0           $plg->config->{Routes}->{$_}->{Protected} = 0
130             }
131             }
132              
133 0           print 'Start time : ', scalar localtime $^T ,"\n";
134 0           print "Main PID : $$\n";
135 0           print 'Run as user : ', (getpwuid($>))[0] ,"\n";
136 0           print 'Authorization method : ', $plg->auth_method ,"\n";
137 0           print 'Session directory : ', $plg->dir_session ,"\n";
138 0           print 'Session idle timeout : ', $plg->Session_timeout ," sec\n";
139 0           print "Module auth dir scripts : $module_dir\n";
140 0           print "version Perl : $^V\n";
141 0           print "version Dancer2 : $Dancer2::VERSION\n";
142 0           print "version WebService : $VERSION\n";
143              
144             # Restore the valid sessions, and delete the expired ones
145 0 0         opendir DIR, $plg->dir_session or die "Could not list session directory $plg->{dir_session} because $!\n";
146              
147 0           foreach my $token (grep ! /^\.+$/, readdir DIR) {
148              
149 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        
150 0           my $lastaccess = ${ Storable::retrieve "$plg->{dir_session}/$token/control/lastaccess" };
  0            
151              
152 0 0         if (time - $lastaccess > $plg->Session_timeout) {
153 0           print "Delete expired session: $token\n";
154 0           system $plg->rm, '-rf', "$plg->{dir_session}/$token"
155             }
156             else {
157 0           $TokenDB{$token}->{data} = {};
158 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            
159 0 0         opendir __TOKEN, "$plg->{dir_session}/$token/data" or die "Could not read session directory $plg->{dir_session}/$token/data because $!\n";
160              
161 0           foreach my $record (grep ! /^\.+$/, readdir __TOKEN) {
162 0           $TokenDB{$token}->{data}->{$record} = Storable::retrieve "$plg->{dir_session}/$token/data/$record";
163 0 0         $TokenDB{$token}->{data}->{$record} = ${ $TokenDB{$token}->{data}->{$record} } if 'SCALAR' eq ref $TokenDB{$token}->{data}->{$record}
  0            
164             }
165              
166 0           close __TOKEN;
167 0           print "Restore session : $token (". scalar(keys %{$TokenDB{$token}->{data}}) ." records)\n"
  0            
168             }
169             }
170             else {
171 0           print "Delete corrupt session: $token\n";
172 0           system $plg->rm,'-rf',"$plg->{dir_session}/$token"
173             }
174             }
175              
176 0           closedir DIR;
177              
178             #print Dumper( $app ) ;exit;
179             #print Dumper( $plg->config->{Routes} ) ;exit;
180             #print Dumper( $plg->auth_config ) ;exit;
181             #print Dumper \%TokenDB; exit;
182             #print "---------\n*". $plg->dir_session ."*\n---------\n";
183              
184              
185             # Hook, BEFORE the main app process the request
186              
187             $app->add_hook(Dancer2::Core::Hook->new(name=>'before_request', code=>sub{
188 0     0     $plg->error(0);
189 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          
190 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          
191 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        
192              
193             # format
194 0           foreach (qw/from to/) {
195              
196 0 0         if (exists $app->request->query_parameters->{$_}) {
197              
198 0 0         if ( $app->request->query_parameters->{$_} =~ $fmt_rgx ) {
199 0           $plg->Format->{$_} = $app->request->query_parameters->{$_}
200             }
201             else {
202 0           $plg->Format->{to} = $plg->config->{'Default format'};
203 0           $app->halt($plg->reply('error'=>"Format parameter $_ ( ".$app->request->query_parameters->{$_}.' ) is not one of the :'. join(', ',keys %Formats)))
204             }
205             }
206             else {
207 0           $plg->Format->{$_} = $plg->config->{'Default format'}
208             }
209             }
210              
211             # add header
212 0           $app->request->header('Content-Type'=> $Formats{$plg->Format->{to}});
213              
214             # route name
215 0 0         if ( $app->request->{route}->{regexp} =~/^\^[\/\\]+(.*?)[\/\\]+\(\?#token.*/ ) { $plg->route_name($1) }
  0 0          
216 0           elsif ( $app->request->{route}->{regexp} =~/^\^[\/\\]+(.*?)\$/ ) { $plg->route_name($1) }
217 0           else { $app->halt($plg->reply('error'=>'Could not recognize the route')) }
218              
219             # Convert the posted string (data), to hash $plg->data
220 0           $plg->data({});
221              
222 0 0         if ($app->request->body) {
223              
224 0           eval {
225 0 0         if ($plg->Format->{from} eq 'json') { $plg->data(Cpanel::JSON::XS::decode_json Encode::encode('UTF-8',$app->request->body)) }
  0 0          
    0          
    0          
    0          
226 0           elsif ($plg->Format->{from} eq 'xml') { $plg->data(XML::Hash::XS::xml2hash $app->request->body) }
227 0           elsif ($plg->Format->{from} eq 'yaml') { $plg->data(YAML::Syck::Load $app->request->body) }
228 0           elsif ($plg->Format->{from} eq 'perl') { $plg->data(eval $app->request->body) }
229 0           elsif ($plg->Format->{from} eq 'human') { my $arrayref;
230              
231 0           while ( $app->request->body =~/(.*)$/gm ) {
232 0           my @array = split /\s*(?:\,| |\t|-->|==>|=>|->|=|;|\|)+\s*/, $1;
233 0 0         next unless @array;
234              
235 0 0         if (@array % 2 == 0) {
236 0           push @{$arrayref}, { @array }
  0            
237             }
238             else {
239 0           push @{$arrayref}, { shift @array => [ @array ] }
  0            
240             }
241             }
242              
243 0 0         $plg->data( 1==scalar @{$arrayref} ? $arrayref->[0] : {'Data'=>$arrayref} )
  0            
244             }
245             };
246              
247 0 0         if ($@) {
248 0           $@ =~s/[\s\v\h]+/ /g;
249 0           $app->halt($plg->reply('error'=>'Data parsing as '.$plg->Format->{from}." failed because $@"))
250             }
251             }
252              
253             # Do not proceed if the posted data are not hash
254 0 0         if ('HASH' ne ref $plg->{data}) {
255              
256 0 0         if ('ARRAY' eq ref $plg->{data}) {
257             $plg->{data} = { data => $plg->{data} }
258 0           }
259             else {
260 0           $app->halt($plg->reply('error'=>'Posted data are not keys or list'))
261             }
262             }
263              
264             # Delete not needed control url parameters
265 0           foreach (qw/from to sort pretty message/) {
266 0           delete $app->request->query_parameters->{$_}
267             }
268              
269             # Use as data any url parameter
270 0           foreach (keys %{$app->request->query_parameters}) {
  0            
271 0           $plg->data->{$_} = $app->request->query_parameters->{$_}
272             }
273              
274 0           }));
275              
276              
277             # Hook ONLY for the protected routes, before the main app do anything
278             # halt if the session is expired, otherelse update the lastaccess
279              
280             $app->add_hook(Dancer2::Core::Hook->new(name=>'before', code=>sub{
281 0 0 0 0     return unless (exists $plg->config->{Routes}->{$plg->route_name}) && $plg->config->{Routes}->{$plg->route_name}->{Protected};
282              
283 0 0         $app->halt($plg->reply('error' => "You must provide a token to use the protected route $plg->{route_name}")) unless exists $plg->data->{token};
284 0 0         $app->halt($plg->reply('error' => 'Invalid token')) unless exists $TokenDB{$plg->data->{token}};
285 0           $dir = $plg->dir_session.'/'.$plg->data->{token};
286              
287 0 0         if (time - $TokenDB{$plg->data->{token}}->{control}->{lastaccess} > $plg->Session_timeout) {
288 0           $plg->error('Session expired because its idle time '.(time - $TokenDB{$plg->data->{token}}->{control}->{lastaccess}).' secs is more than the allowed '.$plg->Session_timeout.' secs');
289 0           system $plg->rm,'-rf',$dir;
290 0           delete $TokenDB{$plg->data->{token}};
291 0           $plg->data({}); # clear user data
292 0           $app->halt($plg->reply)
293             }
294             else {
295             # update the lastaccess
296 0           $TokenDB{$plg->data->{token}}->{control}->{lastaccess} = time;
297 0           Storable::lock_store \$TokenDB{$plg->data->{token}}->{control}->{lastaccess}, "$dir/control/lastaccess"
298             }
299              
300             # Check if the user is member to all the Groups of the route
301              
302 0           foreach (@{$plg->config->{Routes}->{$plg->route_name}->{Groups}}) {
  0            
303              
304 0 0         unless (exists $TokenDB{$plg->data->{token}}->{control}->{groups}->{$_} ) {
305 0           $app->halt($plg->reply('error'=>'Required route groups are '. join(',',@{$plg->config->{Routes}->{$plg->route_name}->{Groups}}) .' your groups are '. join(',', sort keys %{$TokenDB{$plg->data->{token}}->{control}->{groups}})))
  0            
  0            
306             }
307             }
308              
309 0           }));
310              
311              
312              
313             # Built in route /WebService list the routes
314             $app->add_route(
315             regexp => '/WebService',
316             method => 'get',
317             code => sub {
318 0     0     my $Routes = $plg->config->{Routes};
319 0           delete @{$Routes}{qw/WebService login logout/};
  0            
320              
321             $plg->reply(
322             Routes=>{
323 0           'Built in' => [ map {$_} qw(WebService WebService/version WebService/client WebService/about login logout) ],
324             'Application' => {
325 0           'Protected' => [ map {$_} grep $Routes->{$_}->{Protected}, sort keys %{$Routes} ],
  0            
326 0           'Public' => [ map {$_} grep ! $Routes->{$_}->{Protected}, sort keys %{$Routes} ]
  0            
  0            
327             }
328             }
329             )
330             }
331 0           );
332              
333             # Built in route /WebService/:what
334             $app->add_route(
335             regexp => '/WebService/:what?',
336             method => 'get',
337             code => sub {
338 0     0     my $app= shift;
339              
340 0 0         if ( $app->request->param('what') =~/(?i)v/ ) {
    0          
    0          
341 0           $plg->reply(Perl=> $], WebService=> $VERSION, Dancer2=> $Dancer2::VERSION)
342             }
343             elsif ( $app->request->param('what') =~/(?i)a/ ) {
344              
345             $plg->reply(
346             Application => $app->{name},
347 0 0 0       Os => eval{ local $_ = undef; local $/ = undef; open __F, -f '/etc/redhat-release' ? '/etc/redhat-release' : '/etc/issue'; if (fileno __F) { ($_= <__F>)=~s/\s*$//s; $_ = join ' ', split /v/, $_ } close __F; $_ // $^O },
  0 0          
  0            
  0            
  0            
  0            
  0            
  0            
348             'Server bind' => $app->request->env->{SERVER_NAME},
349             'Server port' => $app->request->env->{SERVER_PORT},
350 0           'Service uptime' => time - $^T,
351             'Login idle timeout'=> $plg->Session_timeout,
352             'Auth method' => $plg->auth_method
353             )
354             }
355             elsif ( $app->request->param('what') =~/(?i)c/ ) {
356              
357             $plg->reply(
358             Address => $plg->ClientIP,
359             Port => $app->request->env->{REMOTE_PORT},
360 0           Agent => $app->request->agent,
361             Protocol => $app->request->protocol,
362             'Is secure' => $app->request->secure,
363             'Http method' => $app->request->method,
364             'Header accept' => $app->request->header('accept'),
365             'Parameters url' => join(' ', $app->request->params('query')),
366             'Parameters route'=> join(' ', $app->request->params('route')),
367             'Parameters body' => join(' ', $app->request->params('body'))
368             )
369             }
370             else {
371 0           $plg->reply(error=>'Not existing internal route /WebService/'. $app->request->param('what') )
372             }
373             }
374 0           );
375              
376             # logout and delete the session
377             $app->add_route(
378             regexp => '/logout',
379             method => $_,
380             code => sub {
381              
382 0 0   0     if (exists $TokenDB{$plg->data->{token}}) {
383 0           delete $TokenDB{$plg->data->{token}};
384             system $plg->rm,'-rf',$plg->dir_session.'/'.$plg->data->{token}
385 0           }
386              
387 0           $plg->data({});
388 0           $plg->reply()
389 0           }) foreach 'post','put';
390              
391              
392             # Authentication
393             $app->add_route(
394             regexp => '/login',
395             method => $_,
396             code => sub {
397 0     0     my $app = shift;
398              
399             # Check client IP address against the access rules
400 0           $plg->error('Client IP address '.$plg->ClientIP.' is not allowed');
401              
402 0           for (my $i=0; $i<@{$plg->rules_compiled}; $i++) {
  0            
403              
404 0 0         if ( $plg->ClientIP =~ $plg->rules_compiled->[$i] ) {
405 0           $plg->error(0);
406             last
407 0           }
408             }
409              
410 0 0         $app->halt($plg->reply) if $plg->error;
411              
412              
413             # Check the input parameters
414 0 0         foreach ('username','password') {unless (exists $plg->data->{$_}) { $plg->error("Login failed, you did not pass the mandatory key $_"); $app->halt($plg->reply) }}
  0            
  0            
  0            
415 0 0         if ($plg->data->{username} =~/^\s*$/) { $plg->error('Login failed because the username is blank'); $app->halt($plg->reply) }
  0            
  0            
416 0 0         if ($plg->data->{password} eq '') { $plg->error('Login failed because the password is blank'); $app->halt($plg->reply) }
  0            
  0            
417              
418 0           $plg->error('authorization error');
419 0           my $groups={};
420              
421             # Internal
422 0 0         if ('INTERNAL' eq $plg->auth_method) {
423 0           my $user = $plg->data->{username};
424 0           my $conf = $plg->auth_config;
425              
426 0 0         if (exists $conf->{Accounts}->{$user}) {
427 0 0         if ($conf->{Accounts}->{$user} eq '<any>') {$plg->error(0)} # global password
  0 0          
428 0           elsif ($conf->{Accounts}->{$user} eq $plg->data->{password}) {$plg->error(0)} # normal
429             }
430              
431 0 0 0       if ($plg->error && exists $conf->{Accounts}->{'<any>'}) {
432 0 0         if ($conf->{Accounts}->{'<any>'} eq '<any>') {$plg->error(0)} # global user and global password
  0 0          
433 0           elsif ($conf->{Accounts}->{'<any>'} eq $plg->data->{password}) {$plg->error(0)} # global user and normal password
434             }
435             }
436              
437             # The external auth scripts expect at least the two arguments
438             #
439             # 1) username as hex string (for avoiding shell attacks)
440             # 2) password as hex string
441             #
442             # Script output must be the two lines
443             #
444             # 1) 0 for successful login , or the error message at fail
445             # 2) All the groups that the user belongs
446              
447             else {
448 0           my @output;
449 0           my $command = $plg->auth_command.' '.unpack('H*', $plg->data->{username}).' '.unpack('H*', $plg->data->{password});
450 0 0         if (@{$plg->auth_config->{Arguments}}) { $command .=' '.join ' ', map { "\"$_\"" } @{$plg->auth_config->{Arguments}} }
  0            
  0            
  0            
  0            
451              
452             # Execute the external authorization utility and capture its 3 lines output at @output array
453 0 0         open SHELL, '-|', "$command 2> /dev/null" or die "Could run AuthScript \"$command\" because \"$?\"\n";
454 0           while(<SHELL>) {s/^\s*(.*?)\s*$/$1/; push @output,$_}
  0            
  0            
455 0           close SHELL;
456              
457 0 0         unless (2 == scalar @output) { $plg->error('Expected 2 lines output instead of '.scalar(@output).' at auth method '.$plg->auth_method ); $app->halt($plg->reply) }
  0            
  0            
458 0           $plg->error($output[0]);
459 0           map { $groups->{$_} = 1 } split /,/,$output[1]
  0            
460             }
461              
462 0 0         $app->halt($plg->reply) if $plg->error;
463              
464             # Create the token and session dir
465 0 0         open URANDOM__, '<', '/dev/urandom' or die "\nCould not read device /dev/urandom\n";
466 0           read URANDOM__, my $j, 10;
467 0           close URANDOM__;
468 0           $plg->data->{token} = time.'-'.unpack 'h*',$j;
469 0           my $i=0;
470 0           do { $j = sprintf $plg->data->{token}.'-%02d', $i++ } while ( -e $plg->dir_session ."/$j" );
  0            
471 0           $plg->data->{token}=$j;
472              
473 0           foreach ("$plg->{dir_session}/$plg->{data}->{token}", "$plg->{dir_session}/$plg->{data}->{token}/control", "$plg->{dir_session}/$plg->{data}->{token}/data") {
474              
475 0 0         unless (mkdir $_) {
476 0           $plg->error("Could not create session directory $_ because $!");
477 0           $app->halt($plg->reply)
478             }
479             }
480              
481 0           $TokenDB{$plg->data->{token}}->{data} = {};
482 0           @{$TokenDB{$plg->data->{token}}->{control}}{qw/lastaccess groups username/} = (time,$groups,$plg->data->{username});
  0            
483              
484 0           while (my ($k,$v) = each %{ $TokenDB{$plg->data->{token}}->{control} }) {
  0            
485              
486 0 0         unless ( Storable::lock_store \$v, "$plg->{dir_session}/$plg->{data}->{token}/control/$k" ) {
487 0           $plg->error("Could not store session data $_[$i] because $!");
488 0           $plg->dsl->halt(plg->reply)
489             }
490             }
491              
492 0           $plg->reply('token'=>$plg->data->{token}, 'groups'=>[sort keys %{$groups}])
  0            
493 0           }) foreach 'post', 'put'
494             }
495              
496              
497              
498             # Convert $_[0] hash ref to sting as $plg->reply_text
499             # format of "reply_text" is depended from "to" : json xml yaml perl human
500             #
501             # $plg->__HASH_TO_STRING( $hash_reference )
502             # print $plg->{error} ? $plg->{error} : $plg->{reply_text};
503              
504             sub __HASH_TO_STRING
505             {
506 0     0     my $plg=shift;
507 0           $plg->reply_text('');
508              
509 0           eval {
510              
511 0 0         if ($plg->Format->{to} eq 'json') {
    0          
    0          
    0          
    0          
512 0           $JSON->canonical($plg->sort);
513 0           $JSON->space_before(0);
514              
515 0 0         if ($plg->pretty) {
516 0           $JSON->pretty(1);
517 0           $JSON->space_after(1)
518             }
519             else {
520 0           $JSON->pretty(0);
521 0           $JSON->space_after(0)
522             }
523              
524 0           $plg->{reply_text} = $JSON->encode($_[0])
525             }
526             elsif ($plg->Format->{to} eq 'xml') {
527 0           $XML::Hash::XS::canonical=$plg->sort;
528 0           $XML::Hash::XS::indent=$plg->pretty;
529 0           $plg->{reply_text} = XML::Hash::XS::hash2xml $_[0]
530             }
531             elsif ($plg->Format->{to} eq 'yaml') {
532 0           $YAML::Syck::SortKeys=$plg->sort;
533 0           $plg->{reply_text} = YAML::Syck::Dump $_[0]
534             }
535             elsif ($plg->Format->{to} eq 'perl') {
536 0           $Data::Dumper::Indent=$plg->pretty;
537 0           $Data::Dumper::Sortkeys=$plg->sort;
538 0           $plg->{reply_text} = Data::Dumper::Dumper $_[0]
539             }
540             elsif ($plg->Format->{to} eq 'human') {
541 0     0     $Handler{WALKER}->($_[0], sub {my $val=shift; $val =~s/^\s*(.*?)\s*$/$1/; $plg->{reply_text} .= join('.', @_) ." = $val\n"});
  0            
  0            
  0            
542             $plg->{reply_text} = Encode::encode('utf8', $plg->{reply_text})
543 0           }
544             };
545              
546 0 0         if ($@) {
547 0           $@=~s/[\v\h]+/ /g;
548 0           $plg->error("hash to string convertion failed because $@");
549 0           $plg->reply_text('')
550             }
551             }
552              
553              
554              
555             # Returns a reply as: json, xml, yaml, perl or human
556             # It always include the error
557             #
558             # reply only the error
559             # reply( k1=>'v1', ... ) specific keys , values
560             # reply( { k1=>'v1', ... } ) specific keys , values
561             #
562             sub reply :PluginKeyword
563             {
564 0     0 1 0 my $plg = shift;
565              
566 0 0       0 if (@_) {
567              
568 0 0       0 if (1 == @_) {
569              
570 0 0       0 if ('HASH' eq ref $_[0]) {
571 0         0 $plg->__HASH_TO_STRING({error=> $plg->error, %{$_[0]}} )
  0         0  
572             }
573             else {
574 0         0 $plg->__HASH_TO_STRING({error=> $plg->error, $_[0]=> $plg->data->{$_[0]}})
575             }
576             }
577             else {
578 0         0 $plg->__HASH_TO_STRING({error=> $plg->error, @_} ) # This the normal
579             }
580             }
581             else {
582 0         0 $plg->__HASH_TO_STRING({error=> $plg->error}) # if no argument return only the error
583             }
584              
585 0 0       0 if ($plg->error) {
586 0         0 $plg->__HASH_TO_STRING({error=> $plg->error})
587             }
588              
589 0         0 $plg->dsl->halt( $plg->reply_text )
590 1     1   12 }
  1         2  
  1         4  
591              
592              
593              
594             # Return a list/hash of all or the selected posted keys
595             #
596             # PostData(); # all posted keys/values
597             # my %DATA = PostData('k1', 'k2'); # hash with selected posted keys/values
598             # my @DATA = PostData('k1', 'k2'); # list with selected posted keys/values
599             #
600             sub PostData :PluginKeyword
601             {
602 0     0 1 0 my $plg=shift;
603              
604 0 0       0 if (@_) {
605              
606 0 0       0 if ('HASH' eq ref $plg->data) {
    0          
    0          
607 0         0 %{$plg->data}{grep exists $plg->data->{$_}, @_}
  0         0  
608             }
609             elsif ('ARRAY' eq ref $plg->data) {
610 0         0 my %hash;
611 0         0 @hash{@_} = 1;
612 0         0 grep exists $hash{$_}, @{$plg->data}
  0         0  
613             }
614             elsif ('SCALAR' eq ref $plg->data) {
615              
616 0         0 foreach (@_) {
617 0 0       0 return $_ if $_ eq ${$plg->data}
  0         0  
618             }
619             }
620             else {
621              
622 0         0 foreach (@_) {
623 0 0       0 return $_ if $_ eq $plg->data
624             }
625             }
626             }
627             else {
628 0 0       0 if ('HASH' eq ref $plg->data) { %{$plg->data} }
  0 0       0  
  0 0       0  
629 0         0 elsif ('ARRAY' eq ref $plg->data) { @{$plg->data} }
  0         0  
630 0         0 elsif ('SCALAR' eq ref $plg->data) { ${$plg->data} }
  0         0  
631 0         0 else { $plg->data }
632             }
633 1     1   880 }
  1         2  
  1         3  
634              
635              
636              
637             # Retrieves stored session data
638             #
639             # my %data = SessionGet( 'k1', 'k2', ...); # return a hash of the selected keys
640             # my %data = SessionGet(); # return a hash of all keys
641              
642             sub SessionGet :PluginKeyword
643             {
644 0     0 1 0 my $plg = shift;
645              
646 0 0 0     0 unless ((exists $plg->data->{token}) && (exists $TokenDB{$plg->data->{token}})) {
647 0         0 $plg->error('You need a valid token via login for using session data');
648 0         0 $plg->dsl->halt($plg->reply)
649             }
650              
651 0 0       0 if (0 == scalar @_) {
    0          
652 0         0 %{$TokenDB{$plg->data->{token}}->{data}} # all
  0         0  
653             }
654             elsif ((1 == scalar @_)) {
655              
656 0 0       0 if ('ARRAY' eq ref $_[0]) {
657             # At new Perl versions hash slice %{$TokenDB{$plg->data->{token}}->{data}}{@{$_[0]}}
658 0         0 map {$_ , $TokenDB{$plg->data->{token}}->{data}->{$_}} @{$_[0]}
  0         0  
  0         0  
659             }
660             else {
661 0         0 $_[0] , $TokenDB{$plg->data->{token}}->{data}->{$_[0]} # one record
662             }
663             }
664             else {
665 0         0 map {$_ , $TokenDB{$plg->data->{token}}->{data}->{$_}} @_
  0         0  
666             }
667 1     1   517 }
  1         1  
  1         4  
668              
669              
670             # Set session data
671             # Session data are not volatile like the user data.
672             # They are persistent between requests
673             #
674             # SessionSet( new1 => 'foo1', new2 => 'foo2' );
675             # SessionSet( {new1 => 'foo1', new2 => 'foo2'} );
676              
677             sub SessionSet :PluginKeyword
678             {
679 0     0 1 0 my $plg = shift;
680              
681 0 0 0     0 unless ((exists $plg->data->{token}) && (exists $TokenDB{$plg->data->{token}})) {
682 0         0 $plg->error('You need a vaild token via login for using session data');
683 0         0 $plg->dsl->halt($plg->reply)
684             }
685              
686 0         0 my @keys;
687 0 0 0     0 @_ = %{$_[0]} if (1 == @_) && ('HASH' eq ref $_[0]);
  0         0  
688              
689 0         0 for (my($i,$j)=(0,1); $i < scalar(@_) - (scalar(@_) % 2); $i+=2,$j+=2) {
690 0         0 push @keys, $_[$i];
691              
692 0         0 $TokenDB{$plg->data->{token}}->{data}->{$_[$i]} = $_[$j];
693 0 0       0 my $data = ref $_[$j] ? $_[$j] : \$_[$j];
694              
695 0 0       0 unless ( Storable::lock_store $data, "$plg->{dir_session}/$plg->{data}->{token}/data/$_[$i]" ) {
696 0         0 $plg->error("Could not store session data $_[$i] because $!");
697 0         0 $plg->dsl->halt(plg->reply)
698             }
699             }
700              
701 0         0 'stored keys', \@keys
702 1     1   802 }
  1         2  
  1         3  
703              
704              
705              
706             # Delete session data (not sessions)
707             # It never deletes the built in records : lastaccess, username`
708             #
709             # SessionDel( 'k1', 'k2', ... ); # delete only the selected keys
710             # SessionDel(); # delete all keys
711             #
712             sub SessionDel :PluginKeyword
713             {
714 0     0 1   my $plg = shift;
715              
716 0 0 0       unless ((exists $plg->data->{token}) && (exists $TokenDB{$plg->data->{token}})) {
717 0           $plg->error('You need a vaild token via login for using session data');
718 0           $plg->dsl->halt($plg->reply)
719             }
720              
721 0           $dir = $plg->dir_session.'/'.$plg->data->{token};
722 0           my @keys;
723              
724 0 0         if (@_) {
725 0 0 0       @_ = @{$_[0]} if (1 == @_) && ('ARRAY' eq ref $_[0]);
  0            
726              
727 0           foreach (@_) {
728              
729 0 0         if (exists $TokenDB{$plg->data->{token}}->{data}->{$_}) {
730 0           push @keys,$_;
731 0           delete $TokenDB{$plg->data->{token}}->{data}->{$_};
732 0           unlink "$dir/data/$_"
733             }
734             }
735             }
736             else {
737 0           foreach (keys %{$TokenDB{$plg->data->{token}}->{data}}) {
  0            
738 0           push @keys,$_;
739 0           delete $TokenDB{$plg->data->{token}}->{data}->{$_};
740 0           unlink "$dir/data/$_"
741             }
742             }
743              
744 0           'deleted keys', \@keys
745 1     1   1011 }
  1         2  
  1         4  
746              
747             1
748              
749             __END__
750              
751             =pod
752              
753             =encoding UTF-8
754              
755             =head1 NAME
756              
757             Dancer2::Plugin::WebService - RESTful Web Services with login, persistent data, multiple in/out formats, IP security, role based access
758              
759             =head1 VERSION
760              
761             version 4.4.3
762              
763             =head2 SYNOPSIS
764              
765             The replies have the extra key B<error> . At success B<error> is 0 , while at fail is the error message
766              
767             The posted keys can be placed as url parameters if wanted
768              
769             =head2 Route examples
770              
771             POST login {"username":"joe", "password":"souvlaki"}
772             POST login?username=joe&password=souvlaki
773             POST ViewKeysAll
774             POST ViewKeysSome {"k1":"v1"}
775             POST ProtectStore {"token":"2d85b82b158e", "k1":"v1", "k2":"v2"}
776             POST ProtectDelete {"token":"2d85b82b158e"}
777             POST ProtectRead {"token":"2d85b82b158e"}
778             POST logout {"token":"2d85b82b158e"}
779              
780             =head2 Code
781              
782             package MyApi;
783             use Dancer2;
784             use Dancer2::Plugin::WebService;
785              
786             post '/ViewKeysAll' => sub { reply PostData };
787             post '/ViewKeysSome' => sub { reply PostData('k1','k2') };
788             any '/r3' => sub { my %H = PostData('k1'); reply 'foo'=> $H{k1} };
789             get '/r1' => sub { reply 'k1'=>'v1','k2'=>'v2' };
790             get '/r2' => sub { reply {'k1'=>'v1','k2'=>'v2'}};
791             get '/error' => sub { reply 'k1', 'v1', 'error', 'oups' };
792             any '/ProtectStore' => sub { reply SessionSet('s1'=>'sv1', 's2'=>'v1') };
793             post '/ProtectdDelete' => sub { reply SessionDel('s1', 's2') };
794             any '/ProtectRead' => sub { reply SessionGet('s1', 's2') };
795             dance;
796              
797             =head2 Control output : sort, pretty, to, from
798              
799             You can use the B<to>, B<from>, B<sort>, B<pretty> parameters to change the input/output format
800              
801             I<sort> if true the keys are returned sorted. The default is false because it is faster. Valid values are true, 1, yes, false, 0, no
802              
803             I<pretty> 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
804              
805             I<from> , I<to> define the input/output format. You can mix input/output formats independently. Supported formats are
806              
807             json
808             xml
809             yaml
810             perl
811             human
812              
813             I<from> default is the I<config.yml> property
814              
815             plugins :
816             WebService :
817             Default format : json
818              
819             =head3 Examples
820              
821             GET /SomeRoute?to=human&sort=true&pretty=true
822             GET /SomeRoute?to=perl&sort=true&pretty=false
823             POST /SomeRoute?to=xml&sort=true' {"k1":"v1"}
824             POST /SomeRoute?to=yaml' {"k1":"v1"}
825             POST /SomeRoute?to=perl' {"k1":"v1"}
826             POST /SomeRoute?from=json;to=human' {"k1":"v1"}
827             POST /SomeRoute?from=xml;to=human' <Data><k1>v1</k1></Data>
828             POST /SomeRoute?from=xml;to=yaml' <Data><k1>v1</k1></Data>
829              
830             =head2 Built in routes
831              
832             I<Built in routes>
833              
834             GET /WebService list routes
835             GET /WebService/about about
836             GET /WebService/version version
837             GET /WebService/client client information
838             POST /login get a I<token> for using I<protected> routes and storing I<persistent> data
839             POST /logout If you logout your session and all your persistent data are deleted
840              
841             POST /login {"username":"SomeUser","password":"SomePass"}
842             POST /logout {"token":"SomeToken"}
843              
844             =head2 Routes
845              
846             Your routes can be either B<public> or B<protected>
847              
848             B<public> are the routes that anyone can use without B<login> , Τhey do not support sessions / persistent data. You can access the posted data using the method B<PostData>
849              
850             B<protected> are the routes that you must provide a token, returned by the login route.
851             At B<protected> routes you can I<read>, I<write>, I<delete> persistent data using the methods B<SessionGet> , B<SessionSet> , B<SessionDel>
852              
853             Persistent session data are auto deleted when you B<logout> or if your session expired.
854              
855             You can define a route as B<protected> at the I<config.yml>
856              
857             plugins:
858             WebService:
859             Routes:
860             Route1: { Protected: false }
861             Route2: { Protected: true }
862             Route3: { Protected: true, Groups: [ ftp , storage ] }
863              
864             or at your application code
865              
866             setting('plugins')->{'WebService'}->{'Routes'}->{'SomeRoute'} = { Protected: 'true' };
867              
868             =head2 IP access
869              
870             You can control which clients IP addresses are allowed to login by editing the file I<config.yml>
871              
872             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 * ?
873              
874             plugins:
875             WebService:
876             Allowed hosts:
877             - 127.*
878             - 10.*
879             - 192.168.1.23
880             - 172.20.*
881             - 32.??.34.4?
882             - 4.?.?.??
883             - ????:????:????:6d00:20c:29ff:*:ffa3
884             - "*"
885              
886             =head2 Sessions
887              
888             Upon successful login, client is in session until logout or get expired due to inactivity. In session you can use the session methods by providing the token you received.
889              
890             =head2 Session persistent storage
891              
892             You can change persistent data storage directory at the I<config.yml>
893              
894             plugins:
895             WebService:
896             Session directory : /var/lib/WebService
897              
898             or at your main script
899              
900             setting('plugins')->{'WebService'}->{'Session directory'} = '/var/lib/WebService';
901              
902             Be careful this directory must be writable from the user that is running the service
903              
904             =head3 Session expiration
905              
906             Sessions expired after some seconds of inactivity. You can change the amount of seconds either at the I<config.yml>
907              
908             plugins:
909             WebService:
910             Session idle timeout : 3600
911              
912             or at your main script
913              
914             setting('plugins')->{'WebService'}->{'Session idle timeout'} = 3600;
915              
916             =head2 Methods
917              
918             WebService methods for your main Dancer2 code
919              
920             The posted data can be anything; hashes, lists, scalars
921              
922             curl -X POST 0:/ -d '{ "k1":"v1", "k2":"v2", "k3":"v3" }'
923             curl -X POST 0:/ -d '[ "k1", "k2", "k3", "k4" ]'
924              
925             =head3 PostData
926              
927             Get the posted data
928              
929             PostData Everything posted
930             my %hash = PostData('k2','k4'); Only some keys of a hash
931             my @list = PostData('k2','k4'); Only some items of a list
932              
933             =head3 reply
934              
935             Send the reply to the client; it applies any necessary format convertions.
936             This should be the last route's statement
937              
938             reply only the error
939             reply k1 => 'v1', ... anything you want
940             reply( { k1 => 'v1', ... } ) anything you want
941             reply 'k1' The specific key and its value of the posted data
942              
943             =head3 SessionGet
944              
945             Read session persistent data. I<login is required>
946              
947             my %data = SessionGet; returns a hash of all keys
948             my %data = SessionGet( 'k1', 'k2', ... ); returns a hash of the selected keys
949             my %data = SessionGet(['k1', 'k2', ... ]); returns a hash of the selected keys
950              
951             =head3 SessionSet
952              
953             Store non volatile session persistent data. I<login is required>
954              
955             You must pass your data as key / value pairs
956              
957             SessionSet( 'rec1' => 'v1', 'rec2' => 'v2', ... );
958             SessionSet( { 'rec1' => 'v1', 'rec2' => 'v2', ... } );
959              
960             It returns a document of the stored keys, your can use the url to=... modifier e.g.
961              
962             {
963             "error" : 0,
964             "stored keys" : [ "rec1", "rec2" ]
965             }
966              
967             =head3 SessionDel
968              
969             Deletes session persistent data. I<login is required>
970              
971             SessionDel; delete all keys
972             SessionDel( 'rec1', 'rec2', ... ); delete selected keys
973             SessionDel( [ 'rec1', 'rec2', ... ] ); delete selected keys
974              
975             It returns a document of the deleted keys, your can use the url to=... modifier e.g.
976              
977             {
978             "error" : 0,
979             "deleted keys" : [ "rec1", "rec2" ]
980             }
981              
982             =head2 Authentication andd role based access control
983              
984             For using protected routes, you must provide a valid token received from the B<login> route.
985             The B<login> route is using the the first active authentication method of the I<config.yml>
986             Authentication method can be INTERNAL or external executable Command.
987              
988             At INTERNAL you define the usernames / passwords directly at the I<config.yml> . The <any> means any username or password,
989             so if you want to allow all users to login no matter the username or the password use
990              
991             <any> : <any>
992              
993             This make sense if you just want to give anyone the ability for persistent data
994              
995             At production enviroments, probably you want to use an external auth script e.g for the native "Linux native" authentication
996              
997             MODULE_INSTALL_DIR/AuthScripts/linux.sh
998              
999             The protected routes, at config.yml have Protected:true and their required groups e.g. Groups:[grp1,grp2 ...]
1000              
1001             The user must be member to all the route Groups
1002              
1003             If the route's Groups list is empty or missing, the route will run with any valid token ignoring the group
1004              
1005             This is usefull because you can have role based access control at your routes.
1006             Every user with its token will be able to access only the routes is assigned to.
1007              
1008             A sample route definition. Plese mention the \/ path separator
1009              
1010             Routes:
1011             Route1 :
1012             Protected : false
1013             Route\/foo1 :
1014             Protected : true
1015             Groups : [ group1 , group2 ... ]
1016             Route\/foo2 :
1017             Protected : true
1018             Groups : [ ]
1019              
1020             It is easy to write your own scripts for Active Directory, LDAP, facebook integration or whatever.
1021              
1022             If the Command needs sudo, you must add the user running the WebService to sudoers
1023              
1024             Please read the AUTHENTICATION_SCRIPTS for the details
1025              
1026             A sample I<config.yml> is the following.
1027              
1028             environment : development
1029             plugins :
1030             WebService :
1031             Default format : json
1032             Session directory : /var/lib/WebService
1033             Session idle timeout: 86400
1034             Routes :
1035             mirror : { Protected: false }
1036             somekeys : { Protected: false }
1037             data\/m1 : { Protected: false }
1038             data\/m1 : { Protected: false }
1039             INeedLogin_store : { Protected: true, Groups: [ ftp , storage ] }
1040             INeedLogin_delete : { Protected: true, Groups: log }
1041             INeedLogin_read : { Protected: true }
1042              
1043             Allowed hosts:
1044             - 127.*
1045             - 10.*
1046             - 172.16.?.*
1047             - 192.168.1.23
1048             - "????:????:????:6d00:20c:29ff:*:ffa3"
1049             - "*"
1050              
1051             Authentication methods:
1052             - Name : INTERNAL
1053             Active : true
1054             Accounts :
1055             user1 : pass1
1056             user2 : <any>
1057             <any> : Secret4All
1058              
1059             - Name : Linux native
1060             Active : false
1061             Command : MODULE_INSTALL_DIR/AuthScripts/linux.sh
1062             Arguments : [ ]
1063             Use sudo : true
1064              
1065             - Name : Basic Apache auth for simple users
1066             Active : false
1067             Command : MODULE_INSTALL_DIR/AuthScripts/HttpBasic.sh
1068             Arguments : [ "/etc/htpasswd" ]
1069             Use sudo : false
1070              
1071             =head2 Installation
1072              
1073             You should run your service a non privileged user e.g. I<dancer>
1074              
1075             Create your application ( I<TestService> ) e.g. at I</opt/TestService/>
1076              
1077             dancer2 gen --application TestService --directory TestService --path /opt --overwrite
1078             chown -R dancer:dancer /opt/TestService
1079              
1080             Write your code at the file I</opt/TestService/lib/TestService.pm>
1081              
1082             =head3 Configure your environment file
1083              
1084             I</opt/TestService/environments/development.yml>
1085              
1086             # logger : file, console
1087             # log level : core, debug, info, warning, error
1088              
1089             startup_info : 1
1090             show_errors : 1
1091             warnings : 1
1092             no_server_tokens : 0
1093             log : 'core'
1094             logger : 'console'
1095              
1096             engines:
1097             logger:
1098             file:
1099             log_format : '{"ts":"%{%Y-%m-%d %H:%M:%S}t","host":"%h","level":"%L","message":"%m"}'
1100             log_dir : '/var/log/WebService'
1101             file_name : 'TestService.log'
1102             console:
1103             log_format : '{"ts":"%{%Y-%m-%d %H:%M:%S}t","host":"%h","level":"%L","message":"%m"}'
1104              
1105             Start the service as user I<dancer>
1106              
1107             plackup --host 0.0.0.0 --port 3000 -a /opt/TestService/bin/app.psgi --env production --server Starman --workers=5
1108             plackup --host 0.0.0.0 --port 3000 -a /opt/TestService/bin/app.psgi --env development --server HTTP::Server::PSGI --Reload /opt/TestService/lib/TestService.pm,/opt/TestService/config.yml
1109             plackup --host 0.0.0.0 --port 3000 -a /opt/TestService/bin/app.psgi
1110              
1111             # without Plack
1112             perl /opt/TestService/bin/app.psgi
1113              
1114             view the INSTALL document for details
1115              
1116             =head2 See also
1117              
1118             B<Plack::Middleware::REST> Route PSGI requests for RESTful web applications
1119              
1120             B<Dancer2::Plugin::REST> A plugin for writing RESTful apps with Dancer2
1121              
1122             B<RPC::pServer> Perl extension for writing pRPC servers
1123              
1124             B<RPC::Any> A simple, unified interface to XML-RPC and JSON-RPC
1125              
1126             B<XML::RPC> Pure Perl implementation for an XML-RPC client and server.
1127              
1128             B<JSON::RPC> JSON RPC Server Implementation
1129              
1130             =head1 AUTHOR
1131              
1132             George Bouras <george.mpouras@yandex.com>
1133              
1134             =head1 COPYRIGHT AND LICENSE
1135              
1136             This software is copyright (c) 2023 by George Bouras.
1137              
1138             This is free software; you can redistribute it and/or modify it under
1139             the same terms as the Perl 5 programming language system itself.
1140              
1141             =cut