File Coverage

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