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 )