The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.

NAME

WWW::Suffit::Server - The Suffit API web-server class

SYNOPSIS

    use Mojo::File qw/ path /;

    my $root = path()->child('test')->to_string;
    my $app = MyApp->new(
        project_name => 'MyApp',
        project_version => '0.01',
        moniker => 'myapp',
        debugmode => 1,
        loglevel => 'debug',
        max_history_size => 25,

        # System
        uid => 1000,
        gid => 1000,

        # Dirs and files
        homedir => path($root)->child('share')->make_path->to_string,
        datadir => path($root)->child('var')->make_path->to_string,
        tempdir => path($root)->child('tmp')->make_path->to_string,
        documentroot => path($root)->child('www')->make_path->to_string,
        logfile => path($root)->child('log')->make_path->child('myapp.log')->to_string,
        pidfile => path($root)->child('run')->make_path->child('myapp.pid')->to_string,

        # Server
        server_addr => '*',
        server_port => 8080,
        server_url => 'http://127.0.0.1:8080',
        trustedproxies => ['127.0.0.1'],
        accepts => 10000,
        clients => 1000,
        requests => 100,
        workers => 4,
        spare => 2,
        reload_sig => 'USR2',
        no_daemonize => 1,

        # Security
        mysecret => 'Eph9Ce$quo.p2@oW3',
        rsa_keysize => 2048,
        private_key => undef, # Auto
        public_key => undef, # Auto

        # Initialization options
        all_features    => 'no',
        config_opts     => {
            file => path($root)->child('etc')->make_path->child('myapp.conf')->to_string,
            defaults => {foo => 'bar'},
        },
    );

    # Run preforked application
    $app->preforked_run( 'start' );

    1;

    package MyApp;

    use Mojo::Base 'WWW::Suffit::Server';

    sub init { shift->routes->any('/' => {text => 'Hello World!'}) }

    1;

DESCRIPTION

This module provides API web-server functionality

OPTIONS

    sub startup {
        my $self = shift->SUPER::startup( OPTION_NAME => VALUE, ... );

        # ...
    }

Options passed as arguments to the startup function allow you to customize the initialization of plugins at the level of your descendant class, and options are considered to have higher priority than attributes of the same name.

List of allowed options (pairs of name-value):

admin_routes_opts

    admin_routes_opts => {
        prefix_path => "/admin",
        prefix_name => "admin",
    }
prefix_name
    prefix_name => "admin"

This option defines prefix of admin api route name

Default: 'admin'

prefix_path
    prefix_path => "/admin"

This option defines prefix of admin api route

Default: '/admin'

all_features

    all_features => 'on'

This option enables all of the init_* options, which are described bellow

Default: off

api_routes_opts

    api_routes_opts => {
        prefix_path => "/api",
        prefix_name => "api",
    }
prefix_name
    prefix_name => "api"

This option defines prefix of api route name

Default: 'api'

prefix_path
    prefix_path => "/api"

This option defines prefix of api route

Default: '/api'

authdb_opts

    authdb_opts => {
        uri => "sqlite://<PATH_TO_DB_FILE>?sqlite_unicode=1",
        cachedconnection => 'on',
        cacheexpiration => 300,
        cachemaxkeys => 1024*1024,
        sourcefile => '/tmp/authdb.json',
    }
uri, url
    uri => "sqlite:///tmp/auth.db?sqlite_unicode=1",

Default: See config AuthDBURL or AuthDBURI directive

cachedconnection
    cachedconnection => 'on',

Default: See config AuthDBCachedConnection directive. Default: on

cacheexpire, cacheexpiration
    cacheexpiration => 300,

Default: See config AuthDBCacheExpire or AuthDBCacheExpiration directive. Default: 300

cachemaxkeys
    cachemaxkeys => 1024*1024,

Default: See config AuthDBCacheMaxKeys directive. Default: 1024*1024

sourcefile
    sourcefile => '/tmp/authdb.json',

Default: See config AuthDBSourceFile directive

config_opts

    config_opts => { ... }

This option sets Mojolicious::Plugin::ConfigGeneral plugin options

Default:

    `noload => 1` if $self->configobj exists
    `defaults => $self->config` if $self->config is not void

init_admin_routes

    init_admin_routes => 'on'

This option enables Admin Suffit API routes

Default: off

init_authdb

    init_authdb => 'on'

This option enables AuthDB initialize

Default: off

init_api_routes

    init_api_routes => 'on'

This option enables Suffit API routes

Default: off

init_rsa_keys

    init_rsa_keys => 'on'

This option enables RSA keys initialize

Default: off

init_user_routes

    init_user_routes => 'on'

This option enables User Suffit API routes

Default: off

syslog_opts

    syslog_opts => { ... }

This option sets WWW::Suffit::Plugin::Syslog plugin options

Default:

    `enable => 1` if the `Log` config directive is "syslog"

user_routes_opts

    user_routes_opts => {
        prefix_path => "/user",
        prefix_name => "user",
    }
prefix_name
    prefix_name => "user"

This option defines prefix of user api route name

Default: 'user'

prefix_path
    prefix_path => "/user"

This option defines prefix of user api route

Default: '/user'

ATTRIBUTES

This class implements the following attributes

accepts

    accepts => 0,

Maximum number of connections a worker is allowed to accept, before stopping gracefully and then getting replaced with a newly started worker, passed along to "max_accepts" in Mojo::IOLoop

Default: 10000

See "accepts" in Mojo::Server::Prefork

cache

The WWW::Suffit::Cache object

clients

    clients => 0,

Maximum number of accepted connections this server is allowed to handle concurrently, before stopping to accept new incoming connections, passed along to "max_connections" in Mojo::IOLoop

Default: 1000

See "max_clients" in Mojo::Server::Daemon

configobj

The Config::General object or undef

acruxconfig

The Acrux::Config object or undef

datadir

    datadir => '/var/lib/myapp',

The sharedstate data directory (data dir)

Default: /var/lib/<MONIKER>

debugmode

    debugmode => 0,

If this attribute is enabled then this server is no daemonize performs

documentroot

    documentroot => '/var/www/myapp',

Document root directory

Default: /var/www/<MONIKER>

gid

    gid => 1000,
    gid => getgrnam( 'anonymous' ),

This attribute pass GID to set the real group identifier and the effective group identifier for this process

homedir

    homedir => '/usr/share/myapp',

The Project home directory

Default: /usr/share/<MONIKER>

logfile

    logfile => '/var/log/myapp.log',

The log file

Default: /var/log/<MONIKER>.log

loglevel

    loglevel => 'warn',

This attribute performs set the log level

Default: warn

max_history_size

    max_history_size => 25,

Maximum number of logged messages to store in "history"

Default: 25

moniker

    moniker => 'myapp',

Project name in lowercase notation, project nickname, moniker. This value often used as default filename for configuration files and the like

Default: decamelizing the application class

See "moniker" in Mojolicious

mysecret

    mysecret => 'dgdfg',

Default secret string

Default: <DEFAULT_SECRET>

no_daemonize

    no_daemonize => 1,

This attribute disables the daemonize process

Default: 0

pidfile

    pidfile => '/var/run/myapp.pid',

The pid file

Default: /tmp/prefork.pid

See "pid_file" in Mojo::Server::Prefork

project_name

    project_name => 'MyApp',

The project name. For example: MyApp

Default: current class name

private_key

    private_key => '...'

Private RSA key

project_version

    project_version => '0.01'

The project version. For example: 1.00

NOTE! This is required attribute!

public_key

    public_key => '...',

Public RSA key

requests

    requests => 0,

Maximum number of keep-alive requests per connection

Default: 100

See "max_requests" in Mojo::Server::Daemon

reload_sig

    reload_sig => 'USR2',
    reload_sig => 'HUP',

The signal name that will be used to receive reload commands from the system

Default: USR2

rsa_keysize

    rsa_keysize => 2048

RSA key size

See RSA_KeySize configuration directive

Default: 2048

server_addr

    server_addr => '*',

Main listener address (host)

Default: * (::0, 0:0:0:0)

server_port

    server_port => 8080,

Main listener port

Default: 8080

server_url

    server_url => 'http://127.0.0.1:8080',

Main real listener URL

See ListenAddr and ListenPort configuration directives

Default: http://127.0.0.1:8080

spare

    spare => 0,

Temporarily spawn up to this number of additional workers if there is a need.

Default: 2

See "spare" in Mojo::Server::Prefork

tempdir

    tempdir => '/tmp/myapp',

The temp directory

Default: /tmp/<MONIKER>

trustedproxies

List of trusted proxies

Default: none

uid

    uid => 1000,
    uid => getpwnam( 'anonymous' ),

This attribute pass UID to set the real user identifier and the effective user identifier for this process

workers

    workers => 0,

Number of worker processes

Default: 4

See "workers" in Mojo::Server::Prefork

METHODS

This class inherits all methods from Mojolicious and implements the following new ones

init

    $app->init;

This is your main hook into the Suffit application, it will be called at application startup immediately after calling the Mojolicious startup hook. Meant to be overloaded in a your subclass

listeners

This method returns server listeners as list of URLs

    $prefork->listen( $app->listeners );

preforked_run

    $app->preforked_run( COMMAND );
    $app->preforked_run( COMMAND, ...OPTIONS... );
    $app->preforked_run( COMMAND, { ...OPTIONS... } );
    $app->preforked_run( 'start' );
    $app->preforked_run( 'start', prerun => sub { ... } );
    $app->preforked_run( 'stop' );
    $app->preforked_run( 'restart', prerun => sub { ... } );
    $app->preforked_run( 'status' );
    $app->preforked_run( 'reload' );

This method runs your application using a command that is passed as the first argument

Options:

prerun
    prerun => sub {
        my ($app, $prefork) = @_;

        $prefork->on(finish => sub { # Finish
            my $this = shift; # Prefork object
            my $graceful = shift;
            $this->app->log->debug($graceful
                ? 'Graceful server shutdown'
                : 'Server shutdown'
            );
        });
    }

This option defines callback function that performs operations with prefork instance Mojo::Server::Prefork befor demonize and server running

raise

    $app->raise("Mask %s", "val");
    $app->raise("val");

Prints error message to STDERR and exit with errorlevel = 1

NOTE! For internal use only

reload

The reload hook

startup

Main "startup" in Mojolicious hook

HELPERS

This class implements the following helpers

authdb

This is access method to the AuthDB object (state object)

clientid

    my $clientid = $app->clientid;

This helper returns client ID that calculates from User-Agent and Remote-Address headers:

    md5(User-Agent . Remote-Address)

gen_cachekey

    my $cachekey = $app->gen_cachekey;
    my $cachekey = $app->gen_cachekey(16);

This helper helps generate the new CacheKey for caching user data that was got from authorization database

gen_rsakeys

    my %keysdata = $app->gen_rsakeys;
    my %keysdata = $app->gen_rsakeys( 2048 );

This helper generates RSA keys pair and returns structure as hash:

    private_key => '...',
    public_key  => '...',
    key_size    => 2048,
    error       => '...',

jwt

This helper makes JWT object with RSA keys and returns it

token

This helper performs get of current token from HTTP Request headers

CONFIGURATION

This class supports the following configuration directives

GENERAL DIRECTIVES

Log
    Log         Syslog
    Log         File

This directive defines the log provider. Supported providers: File, Syslog

Default: File

LogFile
    LogFile     /var/log/myapp.log

This directive sets the path to logfile

Default: /var/log/<MONIKER>.log

LogLevel
    LogLevel    warn

This directive defines log level.

Available log levels are trace, debug, info, warn, error and fatal, in that order.

Default: warn

SERVER DIRECTIVES

ListenURL
    ListenURL http://127.0.0.1:8008
    ListenURL http://127.0.0.1:8009
    ListenURL 'https://*:3000?cert=/x/server.crt&key=/y/server.key&ca=/z/ca.crt'

Directives that specify additional listening addresses in URL form

NOTE! This is a multiple directive

Default: none

ListenAddr
    ListenAddr  *
    ListenAddr  0.0.0.0
    ListenAddr  127.0.0.1

This directive sets the master listen address

Default: * (0.0.0.0)

ListenPort
    ListenPort  8080
    ListenPort  80
    ListenPort  443

This directive sets the master listen port

Default: 8080

Accepts
    Accepts     0

Maximum number of connections a worker is allowed to accept, before stopping gracefully and then getting replaced with a newly started worker, defaults to the value of "accepts" in Mojo::Server::Prefork. Setting the value to 0 will allow workers to accept new connections indefinitely

Default: 0

Clients
    Clients     1000

Maximum number of accepted connections each worker process is allowed to handle concurrently, before stopping to accept new incoming connections, defaults to 100. Note that high concurrency works best with applications that perform mostly non-blocking operations, to optimize for blocking operations you can decrease this value and increase "workers" instead for better performance

Default: 1000

Requests
    Requests    100

Maximum number of keep-alive requests per connection

Default: 100

Spare
    Spare       2

Temporarily spawn up to this number of additional workers if there is a need, defaults to 2. This allows for new workers to be started while old ones are still shutting down gracefully, drastically reducing the performance cost of worker restarts

Default: 2

Workers
    Workers     4

Number of worker processes, defaults to 4. A good rule of thumb is two worker processes per CPU core for applications that perform mostly non-blocking operations, blocking operations often require more and benefit from decreasing concurrency with "clients" (often as low as 1)

Default: 4

TrustedProxy
    TrustedProxy  127.0.0.1
    TrustedProxy  10.0.0.0/8
    TrustedProxy  172.16.0.0/12
    TrustedProxy  192.168.0.0/16
    TrustedProxy  fc00::/7

Trusted reverse proxies, addresses or networks in CIDR form. The real IP address takes from X-Forwarded-For header

NOTE! This is a multiple directive

Default: All reverse proxies will be passed

Reload_Sig
    Reload_Sig  USR2
    Reload_Sig  HUP

This directive sets the dafault signal name that will be used to receive reload commands from the system

Default: USR2

SSL/TLS SERVER DIRECTIVES

TLS
    TLS         enabled

This directive enables or disables the TLS (https) listening

Default: disabled

TLS_CA, TLS_Cert, TLS_Key
    TLS_CA      certs/ca.crt
    TLS_Cert    certs/server.crt
    TLS_Key     certs/server.key

Paths to TLS files. Absolute or relative paths (started from /etc/<MONIKER>)

TLS_CA - Path to TLS certificate authority file used to verify the peer certificate. TLS_Cert - Path to the TLS cert file, defaults to a built-in test certificate. TLS_Key - Path to the TLS key file, defaults to a built-in test key

Default: none

TLS_Ciphers, TLS_Verify, TLS_Version
    TLS_Version     TLSv1_2
    TLS_Ciphers     AES128-GCM-SHA256:RC4:HIGH:!MD5:!aNULL:!EDH
    TLS_Verify      0x00

Directives for setting TLS extra data

TLS cipher specification string. For more information about the format see https://www.openssl.org/docs/manmaster/man1/ciphers.html/CIPHER-STRINGS. TLS_Verify - TLS verification mode. TLS_Version - TLS protocol version.

Default: none

TLS_FD, TLS_Reuse, TLS_Single_Accept

TLS_FD - File descriptor with an already prepared listen socket. TLS_Reuse - Allow multiple servers to use the same port with the SO_REUSEPORT socket option. TLS_Single_Accept - Only accept one connection at a time.

SECURITY DIRECTIVES

PrivateKeyFile, PublicKeyFile
    PrivateKeyFile /var/lib/myapp/rsa-private.key
    PublicKeyFile  /var/lib/myapp/rsa-public.key

Private and Public RSA key files If not possible to read files by the specified paths, they will be created automatically

Defaults:

    PrivateKeyFile /var/lib/E<lt>MONIKERE<gt>/rsa-private.key
    PublicKeyFile  /var/lib/E<lt>MONIKERE<gt>/rsa-public.key
RSA_KeySize
    RSA_KeySize     2048

RSA Key size. This is size (length) of the RSA Key. Allowed key sizes in bits: 512, 1024, 2048, 3072, 4096

Default: 2048

Secret
    Secret      "My$ecretPhr@se!"

HMAC secret passphrase

Default: md5(rsa_private_file)

ATHORIZATION DIRECTIVES

AuthDBURL, AuthDBURI
    AuthDBURI "mysql://user:pass@mysql.example.com/authdb \
           ?mysql_auto_reconnect=1&mysql_enable_utf8=1"
    AuthDBURI "sqlite:///var/lib/myapp/auth.db?sqlite_unicode=1"

Authorization database connect string (Data Source URI) This directive written in the URI form

Default: "sqlite:///var/lib/<MONIKER>/auth.db?sqlite_unicode=1"

AuthDBCachedConnection
    AuthDBCachedConnection  1
    AuthDBCachedConnection  Yes
    AuthDBCachedConnection  On
    AuthDBCachedConnection  Enable

This directive defines status of caching while establishing of connection to database

See "cached" in WWW::Suffit::AuthDB

Default: false (no caching connection)

AuthDBCacheExpire, AuthDBCacheExpiration
    AuthDBCacheExpiration    300

The expiration time

See "expiration" in WWW::Suffit::AuthDB

Default: 300 (5 min)

AuthDBCacheMaxKeys
    AuthDBCacheMaxKeys  1024

The maximum keys number in cache

See "max_keys" in WWW::Suffit::AuthDB

Default: 1024*1024 (1`048`576 keys max)

AuthDBSourceFile
    AuthDBSourceFile /var/lib/myapp/authdb.json

Authorization database source file path. This is simple JSON file that contains three blocks: users, groups and realms.

Default: /var/lib/<MONIKER>/authdb.json

Token
    Token   ed23...3c0a

Development token directive This development directive allows authorization without getting real Authorization header from the client request

Default: none

EXAMPLE

Example of well-structured simplified web application

    # mkdir lib
    # touch lib/MyApp.pm
    # chmod 644 lib/MyApp.pm

We will start by MyApp.pm that contains main application class and controller class

    package MyApp;

    use Mojo::Base 'WWW::Suffit::Server';

    our $VERSION = '1.00';

    sub init {
        my $self = shift;
        my $r = $self->routes;
        $r->any('/' => {text => 'Your test server is working!'})->name('index');
        $r->get('/test')->to('example#test')->name('test');
    }

    1;

    package MyApp::Controller::Example;

    use Mojo::Base 'Mojolicious::Controller';

    sub test {
        my $self = shift;
        $self->render(text => 'Hello World!');
    }

    1;

The init method gets called right after instantiation and is the place where the whole your application gets set up

    # mkdir bin
    # touch bin/myapp.pl
    # chmod 644 bin/myapp.pl

myapp.pl itself can now be created as simplified application script to allow running tests.

    #!/usr/bin/perl -w
    use strict;
    use warnings;

    use Mojo::File qw/ curfile path /;

    use lib curfile->dirname->sibling('lib')->to_string;

    use Mojo::Server;

    my $root = curfile->dirname->child('test')->to_string;

    Mojo::Server->new->build_app('MyApp',
        debugmode => 1,
        loglevel => 'debug',
        homedir => path($root)->child('www')->make_path->to_string,
        datadir => path($root)->child('var')->make_path->to_string,
        tempdir => path($root)->child('tmp')->make_path->to_string,
        config_opts     => {
            noload => 1, # force disable loading config from file
            defaults => {
                foo => 'bar',
            },
        },
    )->start();

Now try to run it

    # perl bin/myapp.pl daemon -l http://*:8080

Now let's get to simplified testing

    # mkdir t
    # touch t/myapp.t
    # chmod 644 t/myapp.t

Full Mojolicious applications are easy to test, so t/myapp.t can be containts:

    use strict;
    use warnings;

    use Test::More;
    use Test::Mojo;

    use Mojo::File qw/ path /;

    use MyApp;

    my $root = path()->child('test')->to_string;

    my $t = Test::Mojo->new(MyApp->new(
        homedir => path($root)->child('www')->make_path->to_string,
        datadir => path($root)->child('var')->make_path->to_string,
        tempdir => path($root)->child('tmp')->make_path->to_string,
        config_opts     => {
            noload => 1, # force disable loading config from file
            defaults => {
                foo => 'bar',
            },
        },
    ));

    subtest 'Test workflow' => sub {

        $t->get_ok('/')
          ->status_is(200)
          ->content_like(qr/working!/, 'right content by GET /');

        $t->post_ok('/' => form => {'_' => time})
          ->status_is(200)
          ->content_like(qr/working!/, 'right content by POST /');

        $t->get_ok('/test')
          ->status_is(200)
          ->content_like(qr/World/, 'right content by GET /test');

    };

    done_testing();

Now try to test

    # prove -lv t/myapp.t

And our final directory structure should be looking like this

    MyApp
    +- bin
    |  +- myapp.pl
    +- lib
    |  +- MyApp.pm
    +- t
    |  +- myapp.t
    +- test
       +- tmp
       +- var
       +- www

Test-driven development takes a little getting used to, but can be a very powerful tool

HISTORY

See Changes file

TO DO

See TODO file

SEE ALSO

Mojolicious, WWW::Suffit, WWW::Suffit::RSA, WWW::Suffit::JWT, WWW::Suffit::API, WWW::Suffit::AuthDB

AUTHOR

Serż Minus (Sergey Lepenkov) https://www.serzik.com <abalama@cpan.org>

COPYRIGHT

Copyright (C) 1998-2024 D&D Corporation. All Rights Reserved

LICENSE

This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.

See LICENSE file and https://dev.perl.org/licenses/