WWW-Suffit-Server

 view release on metacpan or  search on metacpan

lib/WWW/Suffit/Server.pm  view on Meta::CPAN

package WWW::Suffit::Server;
use strict;
use warnings;
use utf8;

=encoding utf8

=head1 NAME

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

=head1 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;

=head1 DESCRIPTION

This module provides API web-server functionality

=head1 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):

=head2 admin_routes_opts

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

=over 8

=item prefix_name

    prefix_name => "admin"

This option defines prefix of admin api route name

Default: 'admin'

=item prefix_path

lib/WWW/Suffit/Server.pm  view on Meta::CPAN


    loglevel => 'warn',

This attribute performs set the log level

Default: warn

=head2 max_history_size

    max_history_size => 25,

Maximum number of logged messages to store in "history"

Default: 25

=head2 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 L<Mojolicious/moniker>

=head2 mysecret

    mysecret => 'dgdfg',

Default secret string

Default: <DEFAULT_SECRET>

=head2 no_daemonize

    no_daemonize => 1,

This attribute disables the daemonize process

Default: 0

=head2 pidfile

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

The pid file

Default: /tmp/prefork.pid

See L<Mojo::Server::Prefork/pid_file>

=head2 project_name

    project_name => 'MyApp',

The project name. For example: MyApp

Default: current class name

=head2 private_key

    private_key => '...'

Private RSA key

=head2 project_version

    project_version => '0.01'

The project version. For example: 1.00

B<NOTE!> This is required attribute!

=head2 public_key

    public_key => '...',

Public RSA key

=head2 requests

    requests => 0,

Maximum number of keep-alive requests per connection

Default: 100

See L<Mojo::Server::Daemon/max_requests>

=head2 reload_sig

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

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

Default: USR2

=head2 rsa_keysize

    rsa_keysize => 2048

RSA key size

See C<RSA_KeySize> configuration directive

Default: 2048

=head2 server_addr

    server_addr => '*',

Main listener address (host)

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

=head2 server_port

    server_port => 8080,

Main listener port

lib/WWW/Suffit/Server.pm  view on Meta::CPAN

                ? 'Graceful server shutdown'
                : 'Server shutdown'
            );
        });
    }

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

=back

=head2 raise

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

Prints error message to STDERR and exit with errorlevel = 1

B<NOTE!> For internal use only

=head2 reload

The reload hook

=head2 startup

Main L<Mojolicious/startup> hook

=head1 HELPERS

This class implements the following helpers

=head2 authdb

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

=head2 clientid

    my $clientid = $app->clientid;

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

    md5(User-Agent . Remote-Address)

=head2 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

=head2 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       => '...',

=head2 jwt

This helper makes JWT object with RSA keys and returns it

=head2 token

This helper performs get of current token from HTTP Request headers

=head1 CONFIGURATION

This class supports the following configuration directives

=head2 GENERAL DIRECTIVES

=over 8

=item Log

    Log         Syslog
    Log         File

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

Default: File

=item LogFile

    LogFile     /var/log/myapp.log

This directive sets the path to logfile

Default: /var/log/E<lt>MONIKERE<gt>.log

=item LogLevel

    LogLevel    warn

This directive defines log level.

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

Default: warn

=back

=head2 SERVER DIRECTIVES

=over 8

=item 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

lib/WWW/Suffit/Server.pm  view on Meta::CPAN

=cut

our $VERSION = '1.13';

use Mojo::Base 'Mojolicious';

use Carp qw/ carp croak /;
use POSIX qw//;
use File::Spec;

use Mojo::URL;
use Mojo::File qw/ path /;
use Mojo::Home qw//;
use Mojo::Util qw/ decamelize steady_time md5_sum /; # decamelize(ref($self))
use Mojo::Loader qw/ load_class /;
use Mojo::Server::Prefork;

use Acrux::Util qw/ color parse_time_offset randchars /;
use Acrux::RefUtil qw/ as_array_ref as_hash_ref isnt_void is_true_flag /;

use WWW::Suffit::Const qw/
        :general :security :session :dir :server
        AUTHDBFILE JWT_REGEXP
    /;
use WWW::Suffit::Cache;
use WWW::Suffit::RSA;
use WWW::Suffit::JWT;

use constant {
        MAX_HISTORY_SIZE    => 25,
        DEFAULT_SERVER_URL  => 'http://127.0.0.1:8080',
        DEFAULT_SERVER_ADDR => '*',
        DEFAULT_SERVER_PORT => 8080,
    };

# Common attributes
has 'project_name';         # Anonymous
has 'project_version';      # 1.00
has 'server_url';           # http://127.0.0.1:8080
has 'server_addr';          # * (0.0.0.0)
has 'server_port';          # 8080
has 'debugmode';            # 0
has 'configobj';            # Config::General object
has 'acruxconfig';          # Acrux::Config object
has 'cache' => sub { WWW::Suffit::Cache->new };

# Files and directories
has 'documentroot';         # /var/www/<MONIKER>
has 'homedir';              # /usr/share/<MONIKER>
has 'datadir';              # /var/lib/<MONIKER>
has 'tempdir';              # /tmp/<MONIKER>
has 'logfile';              # /var/log/<MONIKER>.log
has 'pidfile';              # /run/<MONIKER>.pid

# Logging
has 'loglevel' => 'warn';   # warn
has 'max_history_size' => MAX_HISTORY_SIZE;

# Security
has 'mysecret' => DEFAULT_SECRET; # Secret
has 'private_key' => '';    # Private RSA key
has 'public_key' => '';     # Public RSA key
has 'rsa_keysize' => sub { shift->conf->latest("/rsa_keysize") };
has 'trustedproxies' => sub { [grep {length} @{(shift->conf->list("/trustedproxy"))}] };

# Prefork
has 'clients' => sub { shift->conf->latest("/clients") || SERVER_MAX_CLIENTS }; # 10000
has 'requests' => sub { shift->conf->latest("/requests") || SERVER_MAX_REQUESTS}; # 100
has 'accepts' => sub { shift->conf->latest("/accepts") }; # SERVER_ACCEPTS is 0 -- by default not specified
has 'spare' => sub { shift->conf->latest("/spare") || SERVER_SPARE }; # 2
has 'workers' => sub { shift->conf->latest("/workers") || SERVER_WORKERS }; # 4
has 'reload_sig' => sub { shift->conf->latest("/reload_sig") // 'USR2' };
has 'no_daemonize';
has 'uid';
has 'gid';

# Startup options as attributes
has [qw/all_features init_rsa_keys init_authdb init_api_routes init_user_routes init_admin_routes/];
has 'config_opts' => sub { {} };
has 'syslog_opts' => sub { {} };
has 'authdb_opts' => sub { {} };
has 'api_routes_opts' => sub { {} };
has 'user_routes_opts' => sub { {} };
has 'admin_routes_opts' => sub { {} };

sub raise {
    my $self = shift;
    say STDERR color "bright_red" => @_;
    $self->log->error((scalar(@_) == 1) ? shift : sprintf(shift, @_));
    exit 1;
}
sub startup {
    my $self = shift;
    my $opts = @_ ? @_ > 1 ? {@_} : {%{$_[0]}} : {};
    $self->project_name(ref($self)) unless defined $self->project_name;
    $self->project_version($self->VERSION) unless defined $self->project_version;
    $self->raise("Incorrect `project_name`") unless $self->project_name;
    $self->raise("Incorrect `project_version`") unless $self->project_version;
    unshift @{$self->plugins->namespaces}, 'WWW::Suffit::Plugin'; # Add another namespace to load plugins from
    push @{$self->routes->namespaces}, 'WWW::Suffit::Server'; # Add Server routes namespace
    my $all_features = is_true_flag($opts->{all_features} // $self->all_features); # on/off

    # Get all ConfigGeneral configuration attributes
    my $config_opts = as_hash_ref($opts->{config_opts} || $self->config_opts) || {};
    if (my $configobj = $self->configobj) {
        $self->raise("The `configobj` must be Config::General object")
            unless ref($configobj) eq 'Config::General';
        $self->config($configobj->getall); # Set config hash
        $config_opts->{noload} = 1 unless exists $config_opts->{noload};
    }

    # Get all Acrux configuration attributes
    if (my $acruxconfig = $self->acruxconfig) {
        $self->raise("The `acruxconfig` must be Acrux::Config object")
            unless ref($acruxconfig) eq 'Acrux::Config';
        $self->config($acruxconfig->config); # Set config hash
        $config_opts->{noload} = 1 unless exists $config_opts->{noload};
    }

    # Init ConfigGeneral plugin
    unless (exists($config_opts->{noload})) { $config_opts->{noload} = 0 }
    unless (exists($config_opts->{defaults})) { $config_opts->{defaults} = as_hash_ref($self->config) }
    $self->plugin('ConfigGeneral' => $config_opts);

    # Syslog
    my $syslog_opts = as_hash_ref($opts->{syslog_opts} || $self->syslog_opts) || {};
    my $syslogen = ($self->conf->latest('/log') && $self->conf->latest('/log') =~ /syslog/i) ? 1 : 0;
    unless (exists($syslog_opts->{enable})) { $syslog_opts->{enable} = $syslogen };
    $self->plugin('Syslog' => $syslog_opts);

    # PRE REQUIRED Plugins
    $self->plugin('CommonHelpers');

    # Logging
    $self->log->level($self->loglevel || ($self->debugmode ? "debug" : "warn"))
        ->max_history_size($self->max_history_size || MAX_HISTORY_SIZE);
    $self->log->path($self->logfile) if $self->logfile;

    # Helpers
    $self->helper('token'       => \&_getToken);
    $self->helper('jwt'         => \&_getJWT);
    $self->helper('clientid'    => \&_genClientId);
    $self->helper('gen_cachekey'=> \&_genCacheKey);
    $self->helper('gen_rsakeys' => \&_genRSAKeys);

    # DataDir (variable data, caches, temp files and etc.) -- /var/lib/<MONIKER>
    $self->datadir(path(SHAREDSTATEDIR, $self->moniker)->to_string()) unless defined $self->datadir;
    $self->raise("Startup error! Data directory %s not exists", $self->datadir) unless -e $self->datadir;

    # HomeDir (shared static files, default templates and etc.) -- /usr/share/<MONIKER>
    $self->homedir(path(DATADIR, $self->moniker)->to_string()) unless defined $self->homedir;
    $self->home(Mojo::Home->new($self->homedir)); # Switch to installable home directory

    # DocumentRoot (user's static data) -- /var/www/<MONIKER>
    my $documentroot = path(WEBDIR, $self->moniker)->to_string();
    $self->documentroot(-e $documentroot ? $documentroot : $self->homedir) unless defined $self->documentroot;

    # Reset static dirs
    $self->static->paths()->[0] = $self->documentroot; #unshift @{$static->paths}, '/home/sri/themes/blue/public';
    $self->static->paths()->[1] = $self->homedir if $self->documentroot ne $self->homedir;

    # Add renderer path (templates)
    push @{$self->renderer->paths}, $self->documentroot, $self->homedir;

    # Remove system favicon file
    delete $self->static->extra->{'favicon.ico'};

    # Set secret
    $self->mysecret($self->conf->latest("/secret")) if $self->conf->latest("/secret");
    $self->secrets([$self->mysecret]);

    # Init RSA keys (optional)
    if ($all_features || is_true_flag($opts->{init_rsa_keys} // $self->init_rsa_keys)) {
        my $private_key_file = $self->conf->latest("/privatekeyfile") || path($self->datadir, PRIVATEKEYFILE)->to_string;
        my $public_key_file = $self->conf->latest("/publickeyfile") || path($self->datadir, PUBLICKEYFILE)->to_string;
        if ((!-r $private_key_file) and (!-r $public_key_file)) {
            my $rsa = WWW::Suffit::RSA->new();
            $rsa->key_size($self->rsa_keysize) if $self->rsa_keysize;
            $rsa->keygen;
            path($private_key_file)->spew($rsa->private_key)->chmod(0600);
            $self->private_key($rsa->private_key);
            path($public_key_file)->spew($rsa->public_key)->chmod(0644);
            $self->public_key($rsa->public_key);
        } elsif (!-r $private_key_file) {
            $self->raise("Can't read RSA private key file: \"%s\"", $private_key_file);
        } elsif (!-r $public_key_file) {
            $self->raise("Can't read RSA public key file: \"%s\"", $public_key_file);
        } else {
            $self->private_key(path($private_key_file)->slurp);
            $self->public_key(path($public_key_file)->slurp)
        }
    }

    # Init AuthDB plugin (optional)
    if ($all_features || is_true_flag($opts->{init_authdb} // $self->init_authdb)) {
        #_load_module("WWW::Suffit::AuthDB");
        my $authdb_opts = as_hash_ref($opts->{authdb_opts} || $self->authdb_opts) || {};
        my $authdb_file = path($self->datadir, AUTHDBFILE)->to_string;
        my $authdb_uri = $authdb_opts->{uri} || $authdb_opts->{url}
            || $self->conf->latest("/authdburl") || $self->conf->latest("/authdburi")
            || qq{sqlite://$authdb_file?sqlite_unicode=1};
        my $cacheexpiration = $self->conf->latest("/authdbcacheexpire") || $self->conf->latest("/authdbcacheexpiration");
        $self->plugin('AuthDB' => {
            ds          => $authdb_uri,
            cached      => $authdb_opts->{cachedconnection} // $self->conf->latest("/authdbcachedconnection") // 'on',
            expiration  => $authdb_opts->{cacheexpire} || $authdb_opts->{cacheexpiration} ||
                           (defined($cacheexpiration) ? parse_time_offset($cacheexpiration) : undef),
            max_keys    => $authdb_opts->{cachemaxkeys} || $self->conf->latest("/authdbcachemaxkeys"),
            sourcefile  => $authdb_opts->{sourcefile} || $self->conf->latest("/authdbsourcefile"),
        });
        $self->authdb->with_roles(qw/+CRUD +AAA/);
        #$self->log->info(sprintf("AuthDB URI: \"%s\"", $authdb_uri));
    }

    # Set API routes plugin (optional)
    $self->plugin('API' => as_hash_ref($opts->{api_routes_opts} || $self->api_routes_opts) || {})
        if $all_features || is_true_flag($opts->{init_api_routes} // $self->init_api_routes);
    $self->plugin('API::User' => as_hash_ref($opts->{user_routes_opts} || $self->user_routes_opts) || {})
        if $all_features || is_true_flag($opts->{init_user_routes} // $self->init_user_routes);
    $self->plugin('API::Admin' => as_hash_ref($opts->{admin_routes_opts} || $self->admin_routes_opts) || {})
        if $all_features || is_true_flag($opts->{init_admin_routes} // $self->init_admin_routes);

    # Hooks
    $self->hook(before_dispatch => sub {
        my $c = shift;
        $c->res->headers->server(sprintf("%s/%s", $self->project_name, $self->project_version)); # Set Server header
    });

    # Init hook
    $self->init;

    return $self;
}
sub init { } # Overload it
sub reload { # Reload hook
    my $self = shift;
    $self->log->warn("Request for reload $$");
    return 1; # 1 - ok; 0 - error :(
}
sub listeners {
    my $self = shift;

    # Resilver cert file
    my $_resolve_cert_file = sub {
        my $f = shift;
        return $f if File::Spec->file_name_is_absolute($f);
        return File::Spec->catfile(SYSCONFDIR, $self->moniker, $f);
    };

lib/WWW/Suffit/Server.pm  view on Meta::CPAN

            $self->raise("detected strange gid") if !($( eq "$gid $gid" && $) eq "$gid $gid"); # just to be sure
        }
        if (my $uid = $self->uid) {
            POSIX::setuid($uid) or $self->raise("setuid %s failed - %s", $uid, $!);
            $self->raise("detected strange uid") if !($< == $uid && $> == $uid); # just to be sure
        }
    }

    # PreRun callback
    if (my $prerun = $opts->{prerun}) {
        $prerun->($self, $prefork) if ref($prerun) eq 'CODE';
    }

    # Daemonize
    $prefork->daemonize() unless $self->no_daemonize;

    # Running
    print "Running\n";
    $prefork->run();
}

sub _load_module {
    my $module = shift;
    if (my $e = load_class($module)) {
        croak ref($e) ? "Exception: $e" : "The module $module not found!";
    }
    return 1;
}
sub _getToken {
    my $self = shift;

    # Get authorization string from request header
    my $token = $self->req->headers->header(TOKEN_HEADER_NAME) // '';
    if (length($token)) {
        return '' unless $token =~ JWT_REGEXP;
        return $token;
    }

    # Get authorization string from request authorization header
    my $auth_string = $self->req->headers->authorization
        || $self->req->env->{'X_HTTP_AUTHORIZATION'}
        || $self->req->env->{'HTTP_AUTHORIZATION'}
        || '';
    if ($auth_string =~ /(Bearer|Token)\s+(.*)/) {
        $token = $2;
        return '' unless length($token) && $token =~ JWT_REGEXP;
        return $token;
    }

    # In debug mode see "Token" config directive
    if ($self->app->debugmode and $token = $self->conf->latest("/token")) {
        return '' unless $token =~ JWT_REGEXP;
    }

    return $token // '';
}
sub _getJWT {
    my $self = shift;
    return WWW::Suffit::JWT->new(
        secret      => $self->app->mysecret,
        private_key => $self->app->private_key,
        public_key  => $self->app->public_key,
    );
}
sub _genCacheKey {
    my $self = shift;
    my $len = shift || 12;
    return randchars($len);
}
sub _genRSAKeys {
    my $self = shift;
    my $key_size = shift || $self->app->rsa_keysize;
    my $rsa = WWW::Suffit::RSA->new();
       $rsa->key_size($key_size) if $key_size;
       $rsa->keygen;
    my ($private_key, $public_key) = ($rsa->private_key // '', $rsa->public_key // '');
    return (
        private_key => $private_key,
        public_key  => $public_key,
        key_size    => $rsa->key_size,
        error       => $rsa->error
            ? sprintf("Error occurred while generation %s bit RSA keys: %s",  $rsa->key_size // '?', $rsa->error)
            : '',
    );
}
sub _genClientId {
    my $self = shift;
    my $user_agent = $self->req->headers->header('User-Agent') // 'unknown';
    my $remote_address = $self->remote_ip($self->app->trustedproxies)
        || $self->tx->remote_address || '::1';
    # md5(User-Agent . Remote-Address)
    return md5_sum(sprintf("%s%s", $user_agent, $remote_address));
}

1;

__END__



( run in 2.137 seconds using v1.01-cache-2.11-cpan-39bf76dae61 )