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

NAME

Dancer2::Plugin::WebService - Rapid creation of RESTful Web Services with sessions and persistent data

VERSION

version 3.012

NAME

Dancer2::Plugin::WebService Rapid creation of RESTful Web Services with sessions and persistent data

VERSION

version 3.012

SYNOPSIS

Using curl ( https://curl.haxx.se ) as client. At your main script you can use all Dancer2 core methods.

All replies have the extra keys error and errormessage . At success error will be 0 . At fail the error will be non 0 while the errormessage will contain a description of the error

  curl localhost:65535/info/version
  curl localhost:65535/info/version?to=yaml
  curl localhost:65535/info/version?to=perl
  curl -X GET localhost:65535/info/version?to=human

  curl localhost:65535/info/client
  curl localhost:65535/info/client?to=json
  curl localhost:65535/info/client?to=xml

  curl --data '{"k1":"v1", "k2":"v2"}'   localhost:65535/test_mirror
  curl --data '{"k1":"v1", "k2":"v2"}'  'localhost:65535/test_mirror?to=xml'
  curl --data '<D><k1>v1</k1></D>'      'localhost:65535/test_mirror?from=xml;to=human'
  curl --data '{"k1":"v1", "k2":"v2"}'   localhost:65535/test_get_one_key
  curl --data '{"k1":"3",  "k2":"5" }'   localhost:65535/test_get_data
  curl --data '{"k1":"v1", "k2":"v2"}'   localhost:65535/test_new_data

  curl --data '{"k1":"v1", "k2":"v2"}'                localhost:65535/test_session
  curl --data '{"user":"joe", "password":"password"}' localhost:65535/login
  curl --data '{"k1":"v1", "k2":"v2", "SessionID":"13d11def280e37ec2d7fbeca096e7d83"}'  localhost:65535/test_session
  curl --data '{"SessionID" : "13d11def280e37ec2d7fbeca096e7d83"}'  localhost:65535/logout

Your script (example)

  package TestService;
  use     strict;
  use     warnings;
  use     Dancer2;
  use     Dancer2::Plugin::WebService;
  our     $VERSION = setting('plugins')->{WebService}->{Version};


  any '/test_mirror' => sub { RestReply('DATA_USER_SEND') };

  any '/test_get_one_key' => sub { RestReply('k1') };

  any '/test_get_data' => sub {
  my ($var1, $var2) = get_data_user('k1', 'k2');
  RestReply( Total => ($var1 + $var2), Thought => 'Lets add !' )
  };

  any '/test_new_data' => sub { 
  my %data = 
  set_data_user( new1 => 'N1', new2 => 'N2' );
  set_data_user( new3 => 'N3', new4 => 'N4' );
  del_data_user( 'new1' , 'new4' );
  RestReply('DATA_USER_ALL')
  };

  setting('plugins')->{'WebService'}->{'Routes'}->{'test_session'} = 'private';

  any '/test_session' => sub {
  my ($v1, $v2) = get_data_user('k1', 'k2');
                  set_data_session(s1 =>'L1', s2=>'L2', s3=>['L3a', 'L3b']);
                  del_data_session('s7', 's8');
  my @Some      = get_data_session('s1', 's2', 's3', 's7');
  my %All       = get_data_session();
  RestReply(k1=>$v1, k2=>$v2, SesData_A => $Some[2], SesData_b=> [ @Some[0..1] ], SesData_all=> { %All } )
  };

  dance;

POLYMORPHISM

Dancer2::Plugin::WebService can handle as input or output multiple formats

  json
  xml
  yaml
  perl
  human

Define input/output format using the url parameters "to" and "from". If missing the default is json. The "to" is the same as "from" if missing. e.g.

  curl localhost:65535/info/client?to=json
  curl localhost:65535/info/client?to=xml
  curl localhost:65535/info/client?to=yaml
  curl localhost:65535/info/client?to=perl
  curl localhost:65535/info/client?to=human

  curl --data '{"k1":"3", "k2":"30"}'               localhost:65535/test_get_data
  curl --data '{"k1":"3", "k2":"30"}'              'localhost:65535/test_get_data?to=xml'
  curl --data '{"k1":"3", "k2":"30"}'              'localhost:65535/test_get_data?to=yaml'
  curl --data '{"k1":"3", "k2":"30"}'              'localhost:65535/test_get_data?to=perl'
  curl --data '{"k1":"3", "k2":"30"}'              'localhost:65535/test_get_data?from=json;to=human'
  curl --data '<Data><k1>3</k1><k2>30</k2></Data>' 'localhost:65535/test_get_data?from=xml'
  curl --data '<Data><k1>3</k1><k2>30</k2></Data>' 'localhost:65535/test_get_data?from=xml;to=human'
  curl --data '<Data><k1>3</k1><k2>30</k2></Data>' 'localhost:65535/test_get_data?from=xml;to=yaml'

ROUTES

Your routes can be either public or private

public are the routes that anyone can use freely without login , they do not support persistent data, so you if you want to pass some data you must send them.

private are the routes that they need user to login . At private routes you can have read/write/delete/update persistent data. These data are automatic deleted when you logout. Methods get_data_session , set_data_session , del_data_session can be used only at private routes

You can flag a route as private either at the config.yml

  plugins:
    WebService:
      Routes:
        SomeRoute: private

or at your main script

  setting('plugins')->{'WebService'}->{'Routes'}->{'SomeRoute'} = 'private';

BUILT-IN ROUTES

There are some built in routes for your convenience. You can use the "from" and "to" format modifiers if you want

info/version

Service information (public route)

  curl localhost:65535/info/version
  curl localhost:65535/info/version?to=yaml
  curl localhost:65535/info/version?to=xml
  curl localhost:65535/info/version?to=perl
  curl localhost:65535/info/version?to=human

info/client

Client information (public route)

  curl localhost:65535/info/client

info

Redirects to /info/version

login

Login for using private routes and storing persistent data. It is a public route.

  curl -s --data '{"user":"joe","password":"password"}' 'http://localhost:65535/login'

You can control which clients are allowed to login by editing the file config.yml

  plugins:
    WebService:
      Allowed hosts:

          - 127.*
          - 10.*
          - 192.168.1.23
          - 172.20.*
          - 32.??.34.4?
          - 4.?.?.??
          - ????:????:????:6d00:20c:29ff:*:ffa3
          - "*"

logout

It is private route as you can not logout without login . In order to logout you must know the SessionID . If you logout you can not use the private routes and all coresponded session data are deleted.

  curl -X GET --data '{"SessionID":"0a1ad34505076d930c3f76c52645e54b"}'  localhost:65535/logout
  curl -X GET --data '{"SessionID":"0a8e4f0523dafa980ec35bcf29a5cc8c"}' 'localhost:65535/logout?from=json;to=xml'

SESSIONS

The sessions auto expired after some seconds of inactivity. You can change the amount of seconds either at the config.yml

  plugins:
    WebService:     
      Session idle timout : 3600

or at your main script

  setting('plugins')->{'WebService'}->{'Session idle timout'} = 3600;

You can change Session persistent data storage directory at the config.yml

  plugins:
    WebService:
      Session directory : /usr/local/sessions

METHODS

RestReply

send the reply to the client. This should be the last route's statement

  RestReply                           only the error and the errormessage
  RestReply(   k1 => 'v1', ...   )    anything you want
  RestReply( { k1 => 'v1', ... } )    anything you want
  RestReply('DATA_USER_SEND')         data send by the user
  RestReply('DATA_USER_ALL')          data send by the user with any addtions
  RestReply('k1')                     data send by the user only one key

get_data_user

Retrieves data user send to WebService with his client e.g. curl or wget . We use this to do something usefull with user's data. This is normally the method you will use more often

  my ($var1, $var2) = get_data_user('k1', 'k2');    # return the selected keys
  my %hash          = get_data_user();              # return all data as hash

set_data_user

You can define extra data. Try to use this instead of common $variables , @arrays , etc because they are visible to other users of the service !

  my %data = set_data_user(   new1 => 'foo1', new2 => 'foo2'   );   # return the keys
  my %data = set_data_user( { new1 => 'foo1', new2 => 'foo2' } );   # return the keys

del_data_user

Deletes data, think it as the opposite of set_data_user

  del_data_user( 'k1', 'k2', ... );    # delete only the selected keys
  del_data_user();                     # delete all keys

set_data_session

Store persistent session data. Session data are not volatile like the user data between service calls. You have to login prior using this method.

  set_data_session(   new1 => 'foo1', new2 => 'foo2'   );
  set_data_session( { new1 => 'foo1', new2 => 'foo2' } );

get_data_session

Retrieves session data. You have to login prior using this method.

  my %data = get_data_session( 'k1', 'k2', ... );    # return only the selected keys
  my %data = get_data_session();                     # returs all keys

del_data_session

Deletes session data. You have to login prior using this method.

  del_data_session( 'k1', 'k2', ... );   # deletes only the selected keys
  del_data_session();                    # deletes all keys

INSTALLATION

After install Dancer2::Plugin::WebService create your application e.g TestService using the command inside e.g. the /opt folder

  cd /opt
  dancer2 gen --application TestService

Assuming that you will start the service as a non privileged user e.g. joe

  mkdir            /var/log/TestService
  mkdir            /usr/local/sessions
  chown -R joe:joe /opt/TestService
  chown -R joe:joe /var/log/TestService
  chown -R joe:joe /usr/local/sessions

If you want compressed replies edit the file /opt/TestService/bin/app.psgi

  #!/usr/bin/perl
  use FindBin;
  use lib "$FindBin::Bin/../lib";
  use TestService;
  use Plack::Builder;
  builder { enable 'Deflater'; TestService->to_app }

Or if you have slow CPU use uncompressed replies by editing the file /opt/TestService/bin/app.psgi

  #!/usr/bin/perl
  use FindBin;
  use lib "$FindBin::Bin/../lib";
  use TestService;      
  TestService->to_app;

Edit the file .../environments/production.yml

  show_errors      : 1
  startup_info     : 1
  warnings         : 1 
  no_server_tokens : 0
  log              : "core"
  logger           : "file"
  engines          :
    logger         :
      File         :
        log_dir    : "/var/log/TestService"
        file_name  : "activity.log"

Edit the file /opt/TestService/config.yml

  appname      : TestService
  environment  : production
  
  plugins:
    WebService:
      Version             : 1.0.0
      Owner               : Joe Lunchbucket, Joe.Lunchbucket@example.com
      Session directory   : /usr/local/sessions
      Session idle timout : 3600
      Default format      : json
      Command sudo        : /usr/bin/sudo
      Command rm          : /usr/bin/rm
      Routes              :
        test1             : public
        test2             : public
        test3             : private
        
      Allowed hosts:
  
        - 127.*
        - 10.*
        - 192.168.1.23
        - 172.20.*
        - 32.??.34.4?
        - 4.?.?.??
        - ????:????:????:6d00:20c:29ff:*:ffa3
        - "*"
  
      User must belong to one or more of the groups:
  
        - power
        - storage
        - network
  
      Authentication method:
        Always allow login for testing:
          Command  : MODULE_INSTALL_DIR/scripts/AlwaysOk/AlwaysOk.sh
          Active   : no
          Use sudo : no
        Linux native users:
          Command  : MODULE_INSTALL_DIR/scripts/LinuxOS/AuthUser.pl
          Active   : yes
          Use sudo : yes
        Basic Apache auth for simple users:
          Command  : MODULE_INSTALL_DIR/scripts/HttpBasic/users.pl
          Active   : no
          Use sudo : no
        Basic Apache auth for admins:
          Command  : MODULE_INSTALL_DIR/scripts/HttpBasic/admins.pl
          Active   : no
          Use sudo : no
        Active directory:
          Command  : MODULE_INSTALL_DIR/scripts/ActiveDirectory/ActiveDirectory.pl
          Active   : no
          Use sudo : no
        LDAP:
          Command  : MODULE_INSTALL_DIR/scripts/LDAP/LDAP.pl
          Active   : no
          Use sudo : no

Write your code at the file /opt/TestService/lib/TestService.pm

e.g.

  package TestService;
  use     strict;
  use     warnings;
  use     Dancer2;
  use     Dancer2::Plugin::WebService;
  our     $VERSION = setting('plugins')->{WebService}->{Version};

  any '/test_mirror'      => sub { RestReply('DATA_USER_SEND') };
  any '/test_get_one_key' => sub { RestReply('k1') };

  any '/test_get_data'    => sub {
  my ($var1, $var2) = get_data_user('k1', 'k2');
  RestReply( Total => ($var1 + $var2), Thought => 'Lets add !' )
  };

  any '/test_new_data'    => sub { 
  my %data = 
  set_data_user( new1 => 'N1', new2 => 'N2' );
  set_data_user( new3 => 'N3', new4 => 'N4' );
  del_data_user( 'new1' , 'new4' );
  RestReply('DATA_USER_ALL')
  };

  setting('plugins')->{'WebService'}->{'Routes'}->{'test_session'} = 'private';

  any '/test_session' => sub {
  my ($v1, $v2) = get_data_user('k1', 'k2');
                  set_data_session(s1 =>'L1', s2=>'L2', s3=>['L3a', 'L3b']);
                  del_data_session('s7', 's8');
  my @Some      = get_data_session('s1', 's2', 's3', 's7');
  my %All       = get_data_session();
  RestReply(k1=>$v1, k2=>$v2, SesData_A => $Some[2], SesData_b=> [ @Some[0..1] ], SesData_all=> { %All } )
  };

  dance;

Start the service as user Joe listening at port e.g 65535 with the real IP for production and multiple threads

  sudo -u joe /usr/bin/site_perl/plackup --host 172.20.21.20 --port 65535 --server Starman --workers=10 --env production -a /opt/TestService/bin/app.psgi

or during development

  sudo -u joe /usr/bin/site_perl/plackup --port 65535 -a /opt/TestService/bin/app.psgi --Reload /opt/TestService/lib,/opt/TestService/config.yml,/usr/share/perl5/site_perl/Dancer2/Plugin
  sudo -u joe /usr/bin/site_perl/plackup --port 65535 -a /opt/TestService/bin/app.psgi

or without Plack

  sudo -u joe  perl /opt/TestService/bin/app.psgi

if you want to install your WebService application as Linux service please readme the INSTALL

AUTHENTICATION

For using private methods and persistent session data you have to login. login is handled from external scripts/programs . It is very easy to write your own script to have authentication with anything you can imagine. The authentication is configured at config.xml Only one authentication method can ne active at any time. The authentication scripts must be executable from the user running the service. Some scripts need elevated privileges so you need to enable sudo for them. E.g. for the native Linux mechanism we have

    User must belong to one or more of the groups:

      - power
      - storage
      - network

    Authentication method:

      Linux native users:
        Command  : MODULE_INSTALL_DIR/scripts/LinuxOS/AuthUser.pl
        Active   : yes
        Use sudo : yes

That means that if a user do not belong to any of the groups power , storage , network the login will fail . We want to use thie method so we have Active : yes . Also because this work only with root priviliges we have Use sudo : yes

That means if you running the service as user joe , at the file /etc/sudoers must exist a line similar to

  joe ALL=NOPASSWD: /usr/share/perl5/site_perl/Dancer2/Plugin/scripts/LinuxOS/AuthUser.pl

For writing your own script please read the scripts howto.txt and review the existing scripts

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 2.0 Server Implementation

This software is copyright (c) 2017 by George Bouras

It is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.

AUTHOR

George Bouras <george.mpouras@yandex.org>

COPYRIGHT AND LICENSE

This software is copyright (c) 2017 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.