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.