The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.

NAME

Dancer2::Plugin::WebService - RESTful Web Services with login, persistent data, multiple in/out formats, IP security, role based access

VERSION

version 4.4.7

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

Route examples

  POST login           {"username":"joe", "password":"souvlaki"}
  POST login?username=joe&password=souvlaki
  POST ViewKeysAll
  POST ViewKeysSome    {"k1":"v1"}
  POST ProtectStore    {"token":"2d85b82b158e", "k1":"v1", "k2":"v2"}
  POST ProtectDelete   {"token":"2d85b82b158e"}
  POST ProtectRead     {"token":"2d85b82b158e"}
  POST logout          {"token":"2d85b82b158e"}

Code

  package MyApi;
  use     Dancer2;
  use     Dancer2::Plugin::WebService;

  post '/ViewKeysAll'    => sub { reply   PostData };
  post '/ViewKeysSome'   => sub { reply   PostData('k1','k2') };
  any  '/r3'             => sub { my %H = PostData('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  '/ProtectStore'   => sub { reply SessionSet('s1'=>'sv1', 's2'=>'v1') };
  post '/ProtectdDelete' => sub { reply SessionDel('s1', 's2') };
  any  '/ProtectRead'    => sub { reply SessionGet('s1', 's2') };
  dance;

Control 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
  xml
  yaml
  perl
  human

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=perl'               {"k1":"v1"}
  POST /SomeRoute?from=json;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 PostData

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 0:/   -d  '{ "k1":"v1", "k2":"v2", "k3":"v3" }'
  curl -X POST 0:/   -d  '[ "k1", "k2", "k3", "k4" ]'

PostData

Get the posted data

             PostData               Everything posted
  my %hash = PostData('k2','k4');   Only some keys  of a hash
  my @list = PostData('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 

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

SessionSet

Store non volatile session persistent data. login is required

You must pass your data as key / value pairs

  SessionSet(   'rec1' => 'v1', 'rec2' => 'v2', ...   );
  SessionSet( { 'rec1' => 'v1', 'rec2' => 'v2', ... } );

It returns a document of the stored keys, your can use the url to=... modifier e.g.

  {
  "error" : 0,
  "stored keys" : [ "rec1", "rec2" ]
  }

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.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 Active Directory, LDAP, facebook integration or whatever.

If the Command needs sudo, you must add the user running the WebService to sudoers

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.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) 2023 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.