NAME
Dancer2::Plugin::WebService - REST apis with login, persistent data, multiple in/out formats, IP security, role based access
VERSION
version 4.7.2
SYNOPSIS
get '/my_keys' => sub { reply { 'k1'=>'v1' , 'k2'=>'v2' } };
curl $url/my_keys
DESCRIPTION
Create REST APIs with login, logout, persistent session data, IP security, role based access. Multiple input/output supported formats : json , xml , yaml, perl , human Post your data and keys as url parameters or content body text
curl -X GET "$url?k1=v1&k2=v2&k3=v3"
curl -X POST $url -d '{ "k1":"v1", "k2":"v2", "k3":"v3" }'
curl -X POST $url -d '[ "k1", "k2", "k3", "k4" ]'
NAME
Convert your functions to REST api with minimal effort
URL parameters to format the reply
You can use the from, to, sort, pretty parameters to define the input/output format
- from , to
-
Define the input/output format. You can mix input/output formats independently. from default is the config.yml property plugins.TestService.Default format = json Supported formats are
json or jsn yaml or yml xml perl human or text or txt
- 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
METHODS
Plugin methods available for your main Dancer2 code
UserData
Get all or some of the posted data Retruns a hash referense if the data are posted as hash Retruns an array referense if the data are posted as list
UserData Everything
UserData('k1','k2'); Only some keys of the posted hash, list
get '/foo' => sub { reply UserData };
Error
It sets the error. Normally at success error is 0. It does not stop the route execution
any['get','post'] => '/error1' => sub { Error('oh no'); reply UserData };
any['get','post'] => '/error2' => sub { Error('oh no'); reply };
any['get','post'] => '/error3' => sub { reply Error('oh no') };
reply
Accepts a Perl data structure, and under the key "reply" returns a string formated as : json, xml, yaml, perl or human
It also returns any error defined from the Error(...)
Aply any necessary format convertions.
This should be the last route's statement
reply
reply( 'hello world' )
reply( 'a', 'b' , 'c' )
reply( [ 'a', 'b' , 'c' ] )
reply( { k1=>'v1', k1=>'v1' } )
reply( \&SomeFunction )
A typical response is
{
"reply" : { "T" : "42", "wind" : "12" },
"error" : "Cartridge out of ink"
}
SessionSet
Store session persistent data. It is a protected method, login is required
Session data are not volatile like posted data.
They are persistent between requests until they deleted, the user logout or their session get expired.
Returns a list of the stored keys.
You must pass your data as hash or hash reference.
any['get','post'] => '/session_save' => sub {
@arr = SessionSet( k1=>'v1' , k2=>'v2' );
@arr = SessionSet( { k3=>'v3' , k3=>'v4' } );
reply { 'saved keys' => \@arr }
};
Store from a POST passing the login token as url parameter
curl $url/session_save?token=17398-5c8a71b -H "$H" -X POST -d '{"k1":"v1", "k2":"v2", "k3":"v3"}'
SessionGet
Read session persistent data. It is a protected method, login is required
Returns a hash
any['post','put'] => '/session_read' => sub {
my %has1 = SessionGet( 'k1','k2' ); # some records
my %has2 = SessionGet( [ 'k1','k2' ] ); # some records
my %hash = SessionGet(); # all reocords
reply { %hash }
};
curl $url/session_read?token=17398-5c8a71b
SessionDel
Deletes session persistent data. It is a protected method, login is required
Returns a list of the deleted keys
SessionDel; delete all keys
SessionDel( 'rec1', 'rec2', ... ); delete selected keys
SessionDel( [ 'rec1', 'rec2', ... ] ); delete selected keys
any['delete'] => '/session_delete' => sub {
my $arg = UserData();
my @arr = SessionDel( $arg );
reply { 'Deleted keys' => \@arr }
};
curl -X DELETE $url/session_delete?token=17398-5c8a71b -H "$H" -d '["k1","k2","k9"]'
{
"error" : 0,
"reply" : {
"Deleted keys" : [ "k1" , "k2" ]
}
}
Authentication and role based access control
The routes can be either public or protected
- protected
-
routes that you must provide a token, returned by the login route. Afer login you can save, read persistent session data, which they auto deleted when you B<logout> or if your session expired.
- public
-
routes that anyone can use without B<login> , they do not support sessions / persistent data.
Example of route definitions and authentication mechanisms at the config.yml
...
Routes:
mirror : { Protected: false }
text : { Protected: true, Groups: [] }
dev\/commits : { Protected: true, Groups: [ git , ansible ] }
Authentication methods:
- Name : INTERNAL
Active : true
Accounts :
user1 : s3cr3T+PA55sW0rD
user2 : <any>
<any> : S3cREt-F0R-aLl
#<any> : <any>
- Name : Linux native users
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
...
For using protected routes, user 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 way you can have group based access at your routes. Every user is be able to access only the routes is assigned to.
It is easy to write your own scripts for LDAP, Active Directory, OAuth 2.0, etc
If the Command needs sudo, you must add the user running the application to sudoers e.g
dendrodb ALL=(ALL:ALL) NOPASSWD: /usr/share/perl5/site_perl/Dancer2/Plugin/AuthScripts/Linux_native_authentication.sh
Please read the file AUTHENTICATION_SCRIPTS for the details
IP access
You can control which clients are allowed to use your application at 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.*
- 172.20.*
- 32.??.34.4?
- 4.?.?.??
- ????:????:????:6d00:20c:29ff:*:ffa3
- 192.168.0.153
- "*"
Sessions
Upon successful login, the client is in session until logout or its session get expired due to inactivity.
While in session you can access protected routes and save, read, delete session persistent data.
at the config.yml You can change persistent data storage directory and session expiration
- Storage directory
-
Be careful this directory must be writable from the user that is running the service To set the sessions directory plugins: WebService: Session directory : /var/lib/WebService or at your application setting('plugins')->{'WebService'}->{'Session directory'} = '/var/lib/WebService';
- Session expiration
-
Sessions expired after some seconds of inactivity. You can change the amount of seconds either at the I<config.yml> plugins: WebService: Session idle timeout : 3600 or at your main script setting('plugins')->{'WebService'}->{'Session idle timeout'} = 3600;
Built in plugin routes
These are plugin built in routes
WebService version
WebService/routes list the built-in and application routes
WebService/client client propertis
login login
logout logout
Usage examples
export url=http://127.0.0.1:3000
export H='Content-Type: application/json'
curl $url
curl $url/WebService/routes?sort=true
curl $url/WebService/client?sort=true
curl $url/WebService
curl "$url/WebService?to=json&pretty=true&sort=true"
curl $url/WebService?to=yaml
curl "$url/WebService?to=xml&pretty=false"
curl "$url/WebService?to=xml&pretty=true"
curl $url/WebService?to=perl
curl $url/WebService?to=human
Application routes
Based on the code of our TestService ( lib/TestService.pm ) some examples of how to login, logout, and route usage
curl $url/mirror -X POST -H "$H" -d '[ "a", "b", "c" ]'
curl $url/mirror -X GET -d '[ "a", { "k1" : "v1" } ]'
curl "$url/mirror?from=xml&to=yaml" -d '<root><k1>v1</k1><k2>v2</k2><k3></k3></root>'
curl $url/mirror?sort=true -d '{ "k1":"v1", "k2":"v2", "k3":{} }'
curl "$url/mirror?k3=v3&k4=v4&sort=true" -d '{ "k1":"v1", "k2":"v2" }'
curl $url/mirror?to=human -d '{ "k1":"v1", "k2":"v2" }'
curl "$url/mirror?to=json&pretty=true" -d '{ "k1":[ "a","b","c" ] }'
curl "$url/mirror?to=xml&pretty=false" -d '{ "k1":"v1", "k2":"v2" }'
curl $url/mirror?to=yaml -d '{ "k1":[ "a","b","c" ] }'
curl $url/mirror?to=FOO -d '{ "k1":"v1" }'
Login. A successful login returns a token e.g. 17393926-5c8-0
curl -X POST $url/login -H "$H" -d '{"username": "user1", "password": "s3cr3T+PA55sW0rD"}'
Unprotected application routes
curl $url/text -H "$H" -d '{"token":"17393926-5c8-0"}' -X POST
curl $url/text_ref
curl $url/list?pretty=false
curl $url/list_ref?to=yaml
curl $url/list_ref
curl $url/hash
curl $url/function/text
curl $url/function/list
curl $url/function/hash
curl $url/function/text_ref
curl $url/function/list_ref
curl $url/keys_selected?to=yaml -d '{ "k1":"v1", "k2":"v2", "k3":"v3" }'
curl $url/keys_selected?to=yaml -d '[ "k1", "k2", "k3", "k4" ]'
curl "$url/error?to=json&pretty=true" -H "$H" -d '{"k1":"B", "k2":"v2"}'
Protected application routes
curl $url/text
curl $url/text?token=17393926-5c8-0
curl $url/session_save?token=17393926-5c8-0 -H "$H" -X POST -d '{"k1":"v1", "k2":"v2", "k3":"v3"}'
curl $url/session_read?token=17393926-5c8-0
curl $url/session_delete?token=17393926-5c8-0 -H "$H" -X DELETE -d '["k3","k8","k9"]'
curl $url/session_read?token=17393926-5c8-0
Logout
curl $url/logout?token=17393926-5c8-0
curl $url/logout -d '{"token":"17393926-5c8-0"}' -H "$H" -X POST
Plugin Installation
You should your run your APIs as a non privileged user e.g. the "dancer"
getent group dancer >/dev/null || groupadd dancer
getent passwd dancer >/dev/null || useradd -g dancer -l -m -c "Dancer2 WebService" -s $(which nologin) dancer
i=/var/lib/WebService; [ -d $i ] || { mkdir $i; chown -R dancer:dancer $i; }
i=/var/log/WebService; [ -d $i ] || { mkdir $i; chown -R dancer:dancer $i; }
cpanm Dancer2
cpanm Dancer2::Plugin::WebService
Create a sample application e.g. the "TestService"
As best practise we create our applications inside the dancer's home directory
/usr/bin/site_perl/dancer2 version
dancer2 gen --application TestService --directory TestService --path /home/dancer --overwrite
vi /home/dancer/TestService/bin/app.psgi
#!/usr/bin/perl
use strict;
use warnings;
use FindBin;
use lib "$FindBin::Bin/../lib";
use TestService;
use Plack::Builder;
builder {
enable 'Deflater';
# you can have multiple applications on different http paths
mount '/' => TestService->to_app
}
chown -R dancer:dancer /home/dancer/TestService
TestService
For better comprehension of the functionality, review the TestService code
package TestService;
use strict;
use warnings;
use Dancer2;
use Dancer2::Plugin::WebService;
set no_default_middleware => true;
our $VERSION = exists config->{appversion} ? config->{appversion} : '0.0.0.0';
get '/' => sub{send_as html => template 'index' => {'title' => 'TestService'}, {layout=>'main'}};
any['get','post']=> '/mirror'=> sub { reply UserData };
any['get','post']=> '/text' => sub { reply 'hello world' };
get '/text_ref' => sub { reply \'hello world' };
get '/list' => sub { reply 'a', 'b', 'c' };
get '/list_ref' => sub { reply [ 'a', 'b', 'c' ] };
get '/hash' => sub { reply { 'k1'=>'v1' , 'k2'=>'v2' } };
get '/function/text' => sub { reply sub { 'hello world' } };
get '/function/list' => sub { reply sub { 'a', 'b', 'c', 'd' } };
get '/function/hash' => sub { reply sub { {'k1'=>'v1','k2'=>'v2'} } };
get '/function/text_ref' => sub { reply sub { \'hello world' } };
get '/function/list_ref' => sub { reply sub { [ 'a', 'b', 'c', 'd' ] } };
any['get','post','put'] => '/error' => sub { Error('oh no'); reply UserData };
any['get','post','put'] => '/keys_selected' => sub { reply UserData('k1','k2') };
any['get','post','put'] => '/session_save' => sub { reply { 'saved keys' => \@arr } };
any['post','put','get'] => '/session_read' => sub { reply { %hash } };
any['delete'] => '/session_delete' => sub { my $arg = UserData(); reply { 'Deleted keys' => \@arr } };
dance
Configuration config.yml
At the configuration file define the application name, version, routes, their security, and the authorization methods
vi /home/dancer/TestService/config.yml
appname : TestService
appversion : 1.0.1
environment : development
layout : main
charset : UTF-8
template : template_toolkit
engines:
template:
template_toolkit:
EVAL_PERL : 0
start_tag : '<%'
end_tag : '%>'
plugins:
WebService:
Session enable : true
Session directory : /var/lib/WebService
Session idle timeout: 86400
Default format : json
Allowed hosts:
- 172.20.20.*
- "????:????:????:6d00:20c:29ff:*:ffa3"
- 127.*
- 10.*.?.*
- *
Routes:
mirror : { Protected: false }
text : { Protected: true, Groups: [] }
text_ref : { Protected: false }
list : { Protected: false }
list_ref : { Protected: false }
hash : { Protected: false }
function\/text : { Protected: false }
function\/list : { Protected: false }
function\/hash : { Protected: false }
function\/text_ref: { Protected: false }
function\/list_ref: { Protected: false }
keys_selected : { Protected: false }
error : { Protected: false }
session_save : { Protected: true, Groups: [] }
session_read : { Protected: true, Groups: [] }
session_delete : { Protected: true, Groups: [] }
dev\/commits : { Protected: true, Groups: [ git , ansibleremote ] }
Authentication methods:
- Name : INTERNAL
Active : true
Accounts :
user1 : s3cr3T+PA55sW0rD
user2 : <any>
<any> : S3cREt-4-aLl
#<any> : <any>
- Name : Linux native users
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
Start the application
To start it manual as user dancer from the command line
- Production
-
sudo -u dancer plackup --host 0.0.0.0 --port 3000 --server Starman --workers=5 --env development -a /home/dancer/TestService/bin/app.psgi
- While developing
-
sudo -u dancer plackup --host 0.0.0.0 --port 3000 --env development --app /home/dancer/TestService/bin/app.psgi --server HTTP::Server::PSGI
view also the INSTALL document for details
Configure the loggger at the environment file
/home/dancer/TestService/environments/[development|production].yml
log : 'core' # core, debug, info, warning, error
startup_info : 1 # print the banner
show_errors : 1 # if true shows a detailed debug error page
show_stacktrace : 0
warnings : 1 # should Dancer2 consider warnings as critical errors?
no_server_tokens : 1 # disable server tokens in production environments
logger : 'file' # console : to STDOUT , file : to file
engines:
logger:
file:
log_format : '{"ts":"%{%Y-%m-%d %H:%M:%S}t","host":"%h","pid":"%P","level":"%L","message":"%m"}'
log_dir : '/var/log/WebService'
file_name : 'TestService.log'
console:
log_format : '%T , %h, %m'
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) 2025 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.