NAME
Dancer2::Plugin::WebService - RESTful Web Services with login, persistent data, multiple in/out formats, IP security, role based access
VERSION
version 4.5.0
SYNOPSIS
The replies have the extra key error . At success error is 0 , while at fail is the error message
The posted keys can be placed as url parameters if wanted
Application code example
package MyApi;
use Dancer2;
use Dancer2::Plugin::WebService;
post '/ViewPosteData' => sub { reply UserData };
post '/ViewPosteSome' => sub { reply UserData('k1','k2') };
any '/r3' => sub { my %H = UserData('k1'); reply 'foo'=> $H{k1} };
get '/r1' => sub { reply 'k1'=>'v1','k2'=>'v2' };
get '/r2' => sub { reply {'k1'=>'v1','k2'=>'v2'} };
get '/error' => sub { reply 'k1', 'v1', 'error', 'oups' };
any '/SessionStore' => sub { reply SessionSet('s1'=>'sv1', 's2'=>'v1') };
post '/SessiondDelete' => sub { reply SessionDel('s1', 's2') };
any '/SessionRead' => sub { reply SessionGet('s1', 's2') };
dance;
Route examples
POST login {"username":"joe", "password":"souvlaki"}
POST login?username=joe&password=souvlaki
POST ViewPosteData
POST ViewPosteSome {"k1":"v1"}
POST SessionStore {"token":"2d85b82b158e", "k1":"v1", "k2":"v2"}
POST SessionDelete {"token":"2d85b82b158e"}
POST SessionRead {"token":"2d85b82b158e"}
POST logout {"token":"2d85b82b158e"}
Format and control the output : sort, pretty, to, from
You can use the to, from, sort, pretty parameters to change the input/output format
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
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
from , to define the input/output format. You can mix input/output formats independently. Supported formats are
json or jsn
yaml or yml
xml
perl
human or text or txt
from default is the config.yml property
plugins :
WebService :
Default format : json
Examples
GET /SomeRoute?to=human&sort=true&pretty=true
GET /SomeRoute?to=perl&sort=true&pretty=false
POST /SomeRoute?to=xml&sort=true' {"k1":"v1"}
POST /SomeRoute?to=yaml' {"k1":"v1"}
POST /SomeRoute?to=yml' {"k1":"v1"}
POST /SomeRoute?to=perl' {"k1":"v1"}
POST /SomeRoute?from=json;to=human' {"k1":"v1"}
POST /SomeRoute?from=jsn;to=human' {"k1":"v1"}
POST /SomeRoute?from=xml;to=human' <Data><k1>v1</k1></Data>
POST /SomeRoute?from=xml;to=yaml' <Data><k1>v1</k1></Data>
Built in routes
Built in routes
GET /WebService list routes
GET /WebService/about about
GET /WebService/version version
GET /WebService/client client information
POST /login get a I<token> for using I<protected> routes and storing I<persistent> data
POST /logout If you logout your session and all your persistent data are deleted
POST /login {"username":"SomeUser","password":"SomePass"}
POST /logout {"token":"SomeToken"}
Routes
Your routes can be either public or protected
public are the routes that anyone can use without login , Τhey do not support sessions / persistent data. You can access the posted data using the method UserData
protected are the routes that you must provide a token, returned by the login route. At protected routes you can read, write, delete persistent data using the methods SessionGet , SessionSet , SessionDel
Persistent session data are auto deleted when you logout or if your session expired.
You can define a route as protected at the config.yml
plugins:
WebService:
Routes:
Route1: { Protected: false }
Route2: { Protected: true }
Route3: { Protected: true, Groups: [ ftp , storage ] }
or at your application code
setting('plugins')->{'WebService'}->{'Routes'}->{'SomeRoute'} = { Protected: 'true' };
IP access
You can control which clients IP addresses are allowed to login by editing the file config.yml
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 * ?
plugins:
WebService:
Allowed hosts:
- 127.*
- 10.*
- 192.168.1.23
- 172.20.*
- 32.??.34.4?
- 4.?.?.??
- ????:????:????:6d00:20c:29ff:*:ffa3
- "*"
Sessions
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.
Session persistent storage
You can change persistent data storage directory at the config.yml
plugins:
WebService:
Session directory : /var/lib/WebService
or at your main script
setting('plugins')->{'WebService'}->{'Session directory'} = '/var/lib/WebService';
Be careful this directory must be writable from the user that is running the service
Session expiration
Sessions expired after some seconds of inactivity. You can change the amount of seconds either at the config.yml
plugins:
WebService:
Session idle timeout : 3600
or at your main script
setting('plugins')->{'WebService'}->{'Session idle timeout'} = 3600;
Methods
WebService methods for your main Dancer2 code
The posted data can be anything; hashes, lists, scalars
curl -X POST ... -d '{ "k1":"v1", "k2":"v2", "k3":"v3" }'
curl -X POST ... -d '[ "k1", "k2", "k3", "k4" ]'
UserData
Get the posted data
UserData Everything is posted
my %hash = UserData('k2','k4'); Only some keys of a hash
my @list = UserData('k2','k4'); Only some items of a list
reply
Send the reply to the client; it applies any necessary format convertions. This should be the last route's statement
reply only the error
reply k1 => 'v1', ... anything you want
reply( { k1 => 'v1', ... } ) anything you want
reply 'k1' The specific key and its value of the posted data
SessionSet
Store non volatile session persistent data. login is required Session data are not volatile like normal the user posted data. They are persistent between requests until the user logout or its session get expired. Returns a list of the sored keys. You must pass your data as hash or hash reference.
SessionSet( 'rec1' => 'v1', 'rec2' => 'v2', ... );
SessionSet( { 'rec1' => 'v1', 'rec2' => 'v2', ... } );
or at your Dancer2 application
get '/SomeRoute' => sub { reply "session keys" => [ SessionSet( 'k1'=>'v1', 'k2'=>'v2' ) ] };
SessionGet
Read session persistent data. login is required
my %data = SessionGet; returns a hash of all keys
my %data = SessionGet( 'k1', 'k2', ... ); returns a hash of the selected keys
my %data = SessionGet(['k1', 'k2', ... ]); returns a hash of the selected keys
SessionDel
Deletes session persistent data. login is required
SessionDel; delete all keys
SessionDel( 'rec1', 'rec2', ... ); delete selected keys
SessionDel( [ 'rec1', 'rec2', ... ] ); delete selected keys
It returns a document of the deleted keys, your can use the url to=... modifier e.g.
{
"error" : 0,
"deleted keys" : [ "rec1", "rec2" ]
}
Authentication andd role based access control
For using protected routes, you must provide a valid token received from the login route. The login route is using the the first active authentication method of the config.yml Authentication method can be INTERNAL or external executable Command.
At INTERNAL you define the usernames / passwords directly at the config.yml . The <any> means any username or password, so if you want to allow all users to login no matter the username or the password use
<any> : <any>
This make sense if you just want to give anyone the ability for persistent data
At production enviroments, probably you want to use an external auth script e.g for the native "Linux native" authentication
MODULE_INSTALL_DIR/AuthScripts/Linux_native_authentication.sh
The protected routes, at config.yml have Protected:true and their required groups e.g. Groups:[grp1,grp2 ...]
The user must be member to all the route Groups
If the route's Groups list is empty or missing, the route will run with any valid token ignoring the group
This is usefull because you can have role based access control at your routes. Every user with its token will be able to access only the routes is assigned to.
A sample route definition. Plese mention the \/ path separator
Routes:
Route1 :
Protected : false
Route\/foo1 :
Protected : true
Groups : [ group1 , group2 ... ]
Route\/foo2 :
Protected : true
Groups : [ ]
It is easy to write your own scripts for LDAP, Active Directory, OAuth, etc
If the Command needs sudo, you must add the user running the WebService to sudoers e.g
dendrodb ALL=(ALL:ALL) NOPASSWD: /usr/share/perl5/site_perl/Dancer2/Plugin/AuthScripts/Linux_native_authentication.sh
Please read the AUTHENTICATION_SCRIPTS for the details
A sample config.yml is the following.
environment : development
plugins :
WebService :
Default format : json
Session directory : /var/lib/WebService
Session idle timeout: 86400
Routes :
mirror : { Protected: false }
somekeys : { Protected: false }
data\/m1 : { Protected: false }
data\/m1 : { Protected: false }
INeedLogin_store : { Protected: true, Groups: [ ftp , storage ] }
INeedLogin_delete : { Protected: true, Groups: log }
INeedLogin_read : { Protected: true }
Allowed hosts:
- 127.*
- 10.*
- 172.16.?.*
- 192.168.1.23
- "????:????:????:6d00:20c:29ff:*:ffa3"
- "*"
Authentication methods:
- Name : INTERNAL
Active : true
Accounts :
user1 : pass1
user2 : <any>
<any> : Secret4All
- Name : Linux native
Active : false
Command : MODULE_INSTALL_DIR/AuthScripts/Linux_native_authentication.sh
Arguments : [ ]
Use sudo : true
- Name : Basic Apache auth for simple users
Active : false
Command : MODULE_INSTALL_DIR/AuthScripts/HttpBasic.sh
Arguments : [ "/etc/htpasswd" ]
Use sudo : false
Installation
You should run your service a non privileged user e.g. dancer
Create your application ( TestService ) e.g. at /opt/TestService/
dancer2 gen --application TestService --directory TestService --path /opt --overwrite
chown -R dancer:dancer /opt/TestService
Write your code at the file /opt/TestService/lib/TestService.pm
Configure your environment file
/opt/TestService/environments/development.yml
# logger : file, console
# log level : core, debug, info, warning, error
startup_info : 1
show_errors : 1
warnings : 1
no_server_tokens : 0
log : 'core'
logger : 'console'
engines:
logger:
file:
log_format : '{"ts":"%{%Y-%m-%d %H:%M:%S}t","host":"%h","level":"%L","message":"%m"}'
log_dir : '/var/log/WebService'
file_name : 'TestService.log'
console:
log_format : '{"ts":"%{%Y-%m-%d %H:%M:%S}t","host":"%h","level":"%L","message":"%m"}'
Start the service as user dancer
plackup --host 0.0.0.0 --port 3000 -a /opt/TestService/bin/app.psgi --env production --server Starman --workers=5
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
plackup --host 0.0.0.0 --port 3000 -a /opt/TestService/bin/app.psgi
# without Plack
perl /opt/TestService/bin/app.psgi
view the INSTALL document for details
See also
Plack::Middleware::REST Route PSGI requests for RESTful web applications
Dancer2::Plugin::REST A plugin for writing RESTful apps with Dancer2
RPC::pServer Perl extension for writing pRPC servers
RPC::Any A simple, unified interface to XML-RPC and JSON-RPC
XML::RPC Pure Perl implementation for an XML-RPC client and server.
JSON::RPC JSON RPC Server Implementation
AUTHOR
George Bouras <george.mpouras@yandex.com>
COPYRIGHT AND LICENSE
This software is copyright (c) 2024 by George Bouras.
This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.