User Tools

Site Tools


Differences

This shows you the differences between two versions of the page.

Link to this comparison view

vpnsw:start [2018-08-01 12:44] (current)
Line 1: Line 1:
 +===== OpenVPN switcher======
 +
 +It is small web application for openvpn services start/stop and some network monitoring.
 +
 +We must only set directory with openvpn configuration,​ default set as '/​etc/​openvpn'​.
 +
 +<code c /​etc/​vpnsw/​vpnsw.conf>​
 +{
 +"​confdir":"/​etc/​openvpn",​
 +"​loglevel":"​debug"​
 +}
 +</​code>​
 +
 +I wrote first release of the application with around 5-6 hours total to replace [[:​bad:​vpn|a legacy WTF application ]] with similar functions.
 +
 +<​del>​Now I made the app without TLS because it was necessary for install the app on old Ubuntu releases. Later I will shore add TLS.</​del>​
 +
 +Release 0.06 for Debian 8 start with SSL/TLS. Default listen only IPv4
 +
 +====Debian jessie package and source code====
 +  * {{ :​vpnsw:​jessie:​vpnsw_0.06-1.debian.tar.xz }}
 +  * {{ :​vpnsw:​jessie:​vpnsw_0.06.orig.tar.xz }}
 +  * {{ :​vpnsw:​jessie:​vpnsw_0.06-1_all.deb }}
 +
 +====Ubuntu precise package and source code====
 +
 +  * {{ :​vpnsw:​precise:​vpnsw_0.03-1.debian.tar.gz }}
 +  * {{ :​vpnsw:​precise:​vpnsw_0.03.orig.tar.xz }}
 +  * {{ :​vpnsw:​precise:​vpnsw_0.03-1_all.deb }}
 +
 +====Source====
 +  * {{ :​vpnsw:​vpnsw-0.03.tar.xz }}
 +  * {{ :​vpnsw:​vpnsw-0.06.tar.xz }}
 +
 +====Debian Repository ==== 
 +<​code>​
 +deb http://​pure.unix7.org/​ubuntu/​precise precise main
 +</​code>​
 +
 +  * Key:
 +<​code>​
 +# apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 72D340A3
 +</​code>​
 +
 +====Screen====
 +
 +{{ :​screenshot-2017-12-05-10-06-37.png?​nolink |}}
 +
 +====Code====
 +
 +====vpnsw.pl====
 +
 +<code perl vpnsw.pl>​
 +#!@PERL@
 +
 +#​------------
 +#--- CRON ---
 +#​------------
 +
 +package Cron;
 +
 +use strict;
 +use warnings;
 +
 +
 +sub new {
 +    my ($class, %args) = @_;
 +    my $self = {};
 +    bless $self, $class;
 +    return $self;
 +}
 +
 +sub ping {
 +    my $self = shift;
 +    my $res = "​Pong!";​
 +    $res;
 +}
 +
 +1;
 +
 +#​--------------
 +#--- DAEMON ---
 +#​--------------
 +
 +package Daemon;
 +
 +use strict;
 +use warnings;
 +use POSIX qw(getpid setuid setgid geteuid getegid);
 +use Cwd qw(cwd getcwd chdir);
 +use Mojo::Util qw(dumper);
 +
 +sub new {
 +    my $class = shift;
 +    my $self = {};
 +    bless $self, $class;
 +    return $self;
 +}
 +
 +sub fork {
 +    my $self = shift;
 +    my $pid = fork;
 +    if ($pid > 0) {
 +        exit;
 +    }
 +    chdir("/"​);​
 +    open(my $stdout, '>&',​ STDOUT); ​
 +    open(my $stderr, '>&',​ STDERR);
 +    open(STDOUT,​ '>>',​ '/​dev/​null'​);​
 +    open(STDERR,​ '>>',​ '/​dev/​null'​);​
 +    getpid;
 +}
 +
 +1;
 +
 +package VPN;
 +
 +use strict;
 +use warnings;
 +use Mojo::Util qw(dumper);
 +use File::​Basename qw(basename dirname);
 +use POSIX;
 +
 +sub new {
 +    my ($class, $app) = @_;
 +    my $self = {
 +        app => $app,
 +    };
 +    bless $self, $class;
 +    return $self;
 +}
 +
 +sub app {
 +    return shift->​{app};​
 +}
 +
 +sub conf_list {
 +    my ($self, $confdir) = @_;
 +    return undef unless $confdir;
 +
 +    opendir(my $dh, $confdir);
 +    my @list;
 +    while (my $name = readdir($dh)) {
 +        next unless ($name =~ m/​^\w{1,​64}.conf$/​);​
 +        next if -d $name;
 +        push @list, "​$confdir/​$name";​
 +    }
 +    closedir $dh;
 +    return \@list;
 +}
 +
 +sub conf_parse {
 +    my ($self, $filename) = @_;
 +    return undef unless $filename;
 +    return undef unless (-f $filename && -r $filename);
 +
 +    open(my $fh, '<:​encoding(UTF-8)',​ $filename) or return undef;
 +    my %list;
 +    while (my $row = <​$fh>​) {
 +        my $key;
 +        my $value;
 +
 +        ($key, $value) = $row =~ /​^(log)\s{1,​20}([\/​a-z0-9.]{1,​64})/;​
 +        $list{$key} = $value if $key;
 +
 +        ($key, $value) = $row =~ /​^(status)\s{1,​20}([\/​_A-Za-z0-9.]{1,​64})/;​
 +        $list{$key} = $value if $key;
 +
 +        my $val2;
 +        ($key, $value, $val2) = $row =~ /​^(server)\s{1,​20}([\/​0-9.]{1,​64})\s{1,​20}([\/​0-9.]{1,​64})/;​
 +        $list{$key} = "​$value/​$val2"​ if $key;
 +    }
 +    return \%list;
 +}
 +
 +sub stat_parse {
 +    my ($self, $filename) = @_;
 +    return undef unless $filename;
 +    return undef unless (-f $filename && -r $filename);
 +
 +    open(my $fh, '<:​encoding(UTF-8)',​ $filename) or return undef;
 +    my %hash;
 +    while (my $row = <​$fh>​) {
 +        my $key;
 +        my $value;
 +        #Common Name,Real Address,​Bytes Received,​Bytes Sent,​Connected Since
 +        my ($cn, $ipaddr, $recv, $sent, $total, $date) = $row 
 +            =~ /​([\w\-]{1,​64}),​([\/​a-z0-9.]{1,​64}):​([0-9]{1,​16}),​([0-9]{1,​16}),​([0-9]{1,​16}),​([\w\:​ ]{1,20})/;
 +        $hash{$cn}{'​ipaddr'​} = $ipaddr if $cn;
 +        $hash{$cn}{'​recv'​} = $recv if $cn;
 +        $hash{$cn}{'​sent'​} = $sent if $cn;
 +        $hash{$cn}{'​total'​} = $total if $cn;
 +        $hash{$cn}{'​date'​} = $date if $cn;
 +
 +        #Virtual Address,​Common Name,Real Address,​Last Ref
 +        my ($local, $cn2, $ipaddr2, $some, $last) = $row 
 +            =~ /​^([0-9.]{1,​64}),​([\w\-]{1,​64}),​([0-9.]{1,​64}):​([0-9]{1,​16}),​([\w\:​ ]{1,20})/;
 +        $hash{$cn2}{'​local'​} = $local if $local;
 +        $hash{$cn2}{'​last'​} = $last if $local;
 +
 +        my ($net, $cn3, $ipaddr3, $some2, $last2) = $row 
 +            =~ /​^([0-9.]{1,​64}\/​[0-9]{2}),​([\w\-]{1,​64}),​([0-9.]{1,​64}):​([0-9]{1,​16}),​([\w\:​ ]{1,20})/;
 +        push @{$hash{$cn3}{'​net'​}},​ $net if $net;
 +
 +        my ($wclient, $cn4, $ipaddr4, $some3, $last4) = $row 
 +            =~ /​^([0-9.]{1,​64}C),​([\w\-]{1,​64}),​([0-9.]{1,​64}):​([0-9]{1,​16}),​([\w\:​ ]{1,20})/;
 +        $wclient =~ s/C// if $wclient;
 +        push @{$hash{$cn4}{'​wclient'​}},​ $wclient if $wclient;
 +
 +    }
 +    return \%hash;
 +}
 +
 +sub conf_basename {
 +    my ($self, $filename) = @_;
 +    return undef unless $filename;
 +    $filename = basename ($filename, "​.conf"​);​
 +    return $filename if $filename;
 +    return undef;
 +}
 +
 +sub system_comm {
 +    my ($self, $comm) = @_;
 +    return undef unless $comm;
 +    open HR, "$comm |" or return undef;
 +    my $out; 
 +    while (my $str = <HR>) { 
 +        $out .= $str;
 +    };
 +    return $out;
 +}
 +
 +
 +sub service_status {
 +    my ($self, $name) = @_;
 +    return undef unless $name;
 +#    my $out = qx(service openvpn status $name 2>&​1);​
 +    my $out = $self->​system_comm("​sudo service openvpn status $name 2>&​1"​) || '';​
 +#    $self->​app->​log->​info("​service_status:​ Status service $name"​);​
 +    return '​up'​ if $out =~ m/is running/;
 +    return '​down'​ if $out =~ m/is not running/;
 +    return undef;
 +}
 +
 +
 +sub service_start {
 +    my ($self, $name) = @_;
 +    return undef unless $name;
 +#    my $out = qx(service openvpn start $name 2>&​1);​
 +    my $out = $self->​system_comm("​sudo service openvpn start $name 2>&​1"​) || '';​
 +    $self->​app->​log->​info("​service_start:​ Start service $name with result $out"​);​
 +    my $s = $self->​service_status($name) || '';​
 +    return 1 if $s eq '​up';​
 +    undef;
 +}
 +
 +sub service_stop {
 +    my ($self, $name) = @_;
 +    return undef unless $name;
 +#    my $out = qx(service openvpn stop $name 2>&​1);​
 +    my $out = $self->​system_comm("​sudo service openvpn stop $name 2>&​1"​) || '';​
 +    $self->​app->​log->​info("​service_stop:​ Stop service $name with result $out"​);​
 +    my $s = $self->​service_status($name) || '';​
 +    return 1 if $s eq '​down';​
 +    return undef;
 +}
 +
 +
 +sub pack_ipaddr {
 +    my ($self, $addr) = @_;
 +    return undef unless $addr;
 +
 +    my ($ipaddr, $mask) = split "​[/​]",​ $addr;
 +    return undef unless $ipaddr;
 +    return undef unless $mask;
 +
 +    my %mask2cidr = (
 +              '​255.255.255.255' ​  => '​32',​
 +              '​255.255.255.254' ​  => '​31',​
 +              '​255.255.255.252' ​  => '​30',​
 +              '​255.255.255.248' ​  => '​29',​
 +              '​255.255.255.240' ​  => '​28',​
 +              '​255.255.255.224' ​  => '​27',​
 +              '​255.255.255.192' ​  => '​26',​
 +              '​255.255.255.128' ​  => '​25',​
 +              '​255.255.255.0' ​    => '​24',​
 +              '​255.255.254.0' ​    => '​23',​
 +              '​255.255.252.0' ​    => '​22',​
 +              '​255.255.248.0' ​    => '​21',​
 +              '​255.255.240.0' ​    => '​20',​
 +              '​255.255.224.0' ​    => '​19',​
 +              '​255.255.192.0' ​    => '​18',​
 +              '​255.255.128.0' ​    => '​17',​
 +              '​255.255.0.0' ​      => '​16',​
 +              '​255.254.0.0' ​      => '​15',​
 +              '​255.252.0.0' ​      => '​14',​
 +              '​255.248.0.0' ​      => '​13',​
 +              '​255.240.0.0' ​      => '​12',​
 +              '​255.224.0.0' ​      => '​11',​
 +              '​255.192.0.0' ​      => '​10',​
 +              '​255.128.0.0' ​      => '​9',​
 +              '​255.0.0.0' ​        => '​8',​
 +              '​254.0.0.0' ​        => '​7',​
 +              '​252.0.0.0' ​        => '​6',​
 +              '​248.0.0.0' ​        => '​5',​
 +              '​240.0.0.0' ​        => '​4',​
 +              '​224.0.0.0' ​        => '​3',​
 +              '​192.0.0.0' ​        => '​2',​
 +              '​128.0.0.0' ​        => '​1',​
 +    );
 +    my $bitc = $mask2cidr{$mask} || undef;
 +    return undef unless $bitc;
 +    return "​$ipaddr/​$bitc";​
 +}
 +
 +1;
 +
 +package VPNsw::​Controller;​
 +
 +use utf8;
 +use strict;
 +use warnings;
 +use Mojo::Base '​Mojolicious::​Controller';​
 +use Mojo::Util qw(md5_sum dumper quote encode);
 +use Mojo::JSON qw(encode_json decode_json);​
 +use Apache::​Htpasswd;​
 +use File::​Basename qw(fileparse);​
 +
 +sub vpn_list {
 +    my $self = shift;
 +    $self->​render(template => '​vpn-list'​);​
 +}
 +
 +sub vpn_info {
 +    my $self = shift;
 +    $self->​render(template => '​vpn-info'​);​
 +}
 +
 +sub hello {
 +    my $self = shift;
 +    $self->​render(template => '​hello'​);​
 +}
 +
 +sub index {
 +    my $self = shift;
 +    $self->​redirect_to("/​vpn/​list"​);​
 +}
 +
 +sub vpn_all {
 +    my $self = shift;
 +    $self->​render(template => '​vpn-all'​);​
 +}
 +
 +#​----------------
 +#--- AJAX API ---
 +#​----------------
 +
 +sub vpn_start {
 +    my $self = shift;
 +    my $service = $self->​req->​param('​service'​);​
 +    return $self->​render(json => {} ) unless $service;
 +    $self->​app->​vpn->​service_start($service);​
 +    my $status = $self->​app->​vpn->​service_status($service);​
 +    $self->​render(json => {service => $service, status => $status } );
 +}
 +
 +sub vpn_stop {
 +    my $self = shift;
 +    my $service = $self->​req->​param('​service'​);​
 +    return $self->​render(json => {} ) unless $service;
 +    $self->​app->​vpn->​service_stop($service);​
 +    my $status = $self->​app->​vpn->​service_status($service);​
 +    $self->​render(json => {service => $service, status => $status } );
 +}
 +
 +
 +#​--------------------
 +#--- SESSION CONT ---
 +#​--------------------
 +
 +sub pwfile {
 +    my ($self, $pwdfile) = @_;
 +    return $self->​app->​config('​pwdfile'​) unless $pwdfile;
 +    $self->​app->​config(pwfile => $pwdfile);
 +}
 +
 +sub ucheck {
 +    my ($self, $username, $password) = @_;
 +    return undef unless $password;
 +    return undef unless $username;
 +    my $pwdfile = $self->​pwfile or return undef;
 +    my $res = undef;
 +#    eval {
 +        my $ht = Apache::​Htpasswd->​new({ passwdFile => $pwdfile, ReadOnly => 1 });
 +        $res = $ht->​htCheckPassword($username,​ $password);
 +#    };
 +    $res;
 +}
 +
 +sub login {
 +    my $self = shift;
 +    return $self->​redirect_to('/'​) if $self->​session('​username'​);​
 +
 +    my $username = $self->​req->​param('​username'​) || undef;
 +    my $password = $self->​req->​param('​password'​) || undef;
 +
 +    return $self->​render(template => '​login'​) unless $username and $password;
 +
 +    if ($self->​ucheck($username,​ $password)) {
 +        $self->​session(username => $username);
 +        return $self->​redirect_to('/'​);​
 +    }
 +    $self->​render(template => '​login'​);​
 +}
 +
 +sub logout {
 +    my $self = shift;
 +    $self->​session(expires => 1);
 +    $self->​redirect_to('/'​);​
 +}
 +
 +
 +1;
 +
 +#​-----------
 +#--- APP ---
 +#​-----------
 +
 +package VPNsw;
 +
 +use strict;
 +use warnings;
 +use Mojo::Base '​Mojolicious';​
 +
 +sub startup {
 +    my $self = shift;
 +}
 +
 +1;
 +
 +#​------------
 +#--- MAIN ---
 +#​------------
 +
 +use strict;
 +use warnings;
 +
 +use POSIX qw(setuid setgid tzset tzname strftime);
 +use Mojo::​Server::​Prefork;​
 +use Mojo::​IOLoop::​Subprocess;​
 +use Mojo::Util qw(md5_sum b64_decode getopt dumper);
 +use Sys::​Hostname qw(hostname);​
 +use File::​Basename qw(basename dirname);
 +use Apache::​Htpasswd;​
 +use Cwd qw(getcwd abs_path);
 +use EV;
 +
 +my $appname = '​vpnsw';​
 +
 +#​--------------
 +#--- GETOPT ---
 +#​--------------
 +
 +getopt
 +    '​h|help'​ => \my $help,
 +    '​c|config=s'​ => \my $conffile,
 +    '​f|nofork'​ => \my $nofork,
 +    '​u|user=s'​ => \my $user,
 +    '​g|group=s'​ => \my $group;
 +
 +
 +if ($help) {
 +    print qq(
 +Usage: app [OPTIONS]
 +
 +Options
 +    -h | --help ​          This help
 +    -c | --config=path ​   Path to config file
 +    -u | --user=user ​     System owner of process
 +    -g | --group=group ​   System group 
 +    -f | --nofork ​        Dont fork process
 +
 +The options override options from configuration file
 +    )."​\n";​
 +    exit 0;
 +}
 +
 +
 +my $server = Mojo::​Server::​Prefork->​new;​
 +my $app = $server->​build_app('​VPNsw'​);​
 +$app = $app->​controller_class('​VPNsw::​Controller'​);​
 +
 +$app->​secrets(['​6d578e43ba88260e0375a1a35fd7954b'​]);​
 +$app->​static->​paths(['​@APP_LIBDIR@/​public'​]);​
 +$app->​renderer->​paths(['​@APP_LIBDIR@/​templs'​]);​
 +
 +$app->​config(conffile => $conffile || '​@APP_CONFDIR@/​vpnsw.conf'​);​
 +$app->​config(pwdfile => '​@APP_CONFDIR@/​vpnsw.pw'​);​
 +$app->​config(logfile => '​@APP_LOGDIR@/​vpnsw.log'​);​
 +$app->​config(loglevel => '​info'​);​
 +$app->​config(pidfile => '​@APP_RUNDIR@/​vpnsw.pid'​);​
 +$app->​config(crtfile => '​@APP_CONFDIR@/​vpnsw.crt'​);​
 +$app->​config(keyfile => '​@APP_CONFDIR@/​vpnsw.key'​);​
 +
 +$app->​config(listenaddr4 => '​0.0.0.0'​);​
 +#​$app->​config(listenaddr6 => '​[::​]'​);​
 +$app->​config(listenport => '​1007'​);​
 +
 +$app->​config(user => $user || '​@APP_USER@'​);​
 +$app->​config(group => $group || '​@APP_GROUP@'​);​
 +
 +$app->​config(confdir => "/​etc/​openvpn"​);​
 +
 +if (-r $app->​config('​conffile'​)) {
 +    $app->​log->​debug("​Load configuration from "​.$app->​config('​conffile'​));​
 +    $app->​plugin('​JSONConfig',​ { file => $app->​config('​conffile'​) });
 +}
 +
 +#​---------------
 +#--- HELPERS ---
 +#​---------------
 +$app->​helper(
 +    vpn => sub {
 +        state $vpn = VPN->​new($app); ​
 +});
 +
 +$app->​helper(
 +    cron => sub {
 +        my $cron = Cron->​new;​
 +        $cron;
 +});
 +
 +$app->​helper('​reply.not_found'​ => sub {
 +        my $c = shift; ​
 +        return $c->​redirect_to('/​login'​) unless $c->​session('​username'​); ​
 +        $c->​render(template => '​not_found.production'​);​
 +});
 +
 +
 +#​--------------
 +#--- ROUTES ---
 +#​--------------
 +
 +my $r = $app->​routes;​
 +
 +$r->​add_condition(
 +    auth => sub {
 +        my ($route, $c) = @_;
 +        $c->​session('​username'​);​
 +    }
 +);
 +
 +$r->​any('/​login'​)->​to('​controller#​login'​);​
 +$r->​any('/​logout'​)->​to('​controller#​logout'​);​
 +
 +$r->​any('/'​)->​over('​auth'​)->​to('​controller#​index'​ );
 +$r->​any('/​hello'​)->​over('​auth'​)->​to('​controller#​hello'​);​
 +
 +$r->​any('/​vpn/​list'​)->​over('​auth'​)->​to('​controller#​vpn_list'​);​
 +$r->​any('/​vpn/​info'​)->​over('​auth'​)->​to('​controller#​vpn_info'​);​
 +$r->​any('/​vpn/​all'​)->​over('​auth'​)->​to('​controller#​vpn_all'​);​
 +
 +$r->​any('/​j/​vpn/​start'​)->​over('​auth'​)->​to('​controller#​vpn_start'​);​
 +$r->​any('/​j/​vpn/​stop'​)->​over('​auth'​)->​to('​controller#​vpn_stop'​);​
 +$r->​any('/​j/​vpn/​status'​)->​over('​auth'​)->​to('​controller#​vpn_status'​);​
 +$r->​any('/​j/​vpn/​list'​)->​over('​auth'​)->​to('​controller#​vpn_status'​);​
 +
 +#​----------------
 +#--- LISTENER ---
 +#​----------------
 +
 +my $tls = '?';​
 +$tls .= '​cert='​.$app->​config('​crtfile'​);​
 +$tls .= '&​key='​.$app->​config('​keyfile'​);​
 +
 +my $listen4;
 +if ($app->​config('​listenaddr4'​)) {
 +    $listen4 = "​http://";​
 +    $listen4 .= $app->​config('​listenaddr4'​).':'​.$app->​config('​listenport'​);​
 +#    $listen4 .= $tls;
 +}
 +
 +my $listen6;
 +if ($app->​config('​listenaddr6'​)) {
 +    $listen6 = "​http://";​
 +    $listen6 .= $app->​config('​listenaddr6'​).':'​.$app->​config('​listenport'​);​
 +#    $listen6 .= $tls;
 +}
 +
 +my @listen;
 +push @listen, $listen4 if $listen4;
 +push @listen, $listen6 if $listen6;
 +
 +$server->​listen(\@listen);​
 +$server->​heartbeat_interval(3);​
 +$server->​heartbeat_timeout(60);​
 +
 +
 +#​-----------------
 +#--- DOEMINIZE ---
 +#​-----------------
 +
 +unless ($nofork) {
 +    my $d = Daemon->​new;​
 +    my $user = $app->​config('​user'​);​
 +    my $group = $app->​config('​group'​);​
 +    $d->​fork;​
 +    $app->​log(Mojo::​Log->​new( ​
 +                path => $app->​config('​logfile'​),​
 +                level => $app->​config('​loglevel'​)
 +    ));
 +}
 +
 +$server->​pid_file($app->​config('​pidfile'​));​
 +
 +#​---------------
 +#--- WEB LOG ---
 +#​---------------
 +
 +$app->​hook(before_dispatch => sub {
 +        my $c = shift;
 +
 +        my $remote_address = $c->​tx->​remote_address;​
 +        my $method = $c->​req->​method;​
 +
 +        my $base = $c->​req->​url->​base->​to_string;​
 +        my $path = $c->​req->​url->​path->​to_string;​
 +        my $loglevel = $c->​app->​log->​level;​
 +        my $url = $c->​req->​url->​to_abs->​to_string;​
 +
 +        unless ($loglevel eq '​debug'​) {
 +            #​$c->​app->​log->​info("​$remote_address $method $base$path"​);​
 +            $c->​app->​log->​info("​$remote_address $method $url"​);​
 +        }
 +        if ($loglevel eq '​debug'​) {
 +            $c->​app->​log->​debug("​$remote_address $method $url"​);​
 +        }
 +});
 +
 +#​----------------------
 +#--- SIGNAL HANDLER ---
 +#​----------------------
 +
 +local $SIG{HUP} = sub {
 +    $app->​log->​info('​Catch HUP signal'​); ​
 +    $app->​log(Mojo::​Log->​new(
 +                    path => $app->​config('​logfile'​),​
 +                    level => $app->​config('​loglevel'​)
 +    ));
 +};
 +
 +
 +my $sub = Mojo::​IOLoop::​Subprocess->​new;​
 +$sub->​run(
 +    sub {
 +        my $subproc = shift;
 +        my $loop = Mojo::​IOLoop->​singleton;​
 +        my $id = $loop->​recurring(
 +            1200 => sub {
 +                my $res = $app->​cron->​ping;​
 +                $app->​log->​info($res);​
 +            }
 +        );
 +        $loop->​start unless $loop->​is_running;​
 +        1;
 +    },
 +    sub {
 +        my ($subprocess,​ $err, @results) = @_;
 +        $app->​log->​info('​Exit subprocess'​);​
 +        1;
 +    }
 +);
 +
 +my $pid = $sub->​pid;​
 +$app->​log->​info("​Subrocess $pid start ");
 +
 +$server->​on(
 +    finish => sub {
 +        my ($prefork, $graceful) = @_;
 +        $app->​log->​info("​Subrocess $pid stop"​);​
 +        kill('​INT',​ $pid);
 +    }
 +);
 +
 +$server->​run;​
 +#EOF
 +</​code>​
 +
 +====templs/​exception.development.html.ep====
 +
 +<code perl templs/​exception.development.html.ep>​
 +%#
 +%# $Id: exception.html.ep 627 2017-04-15 13:02:08Z ziggi $
 +%#
 +% layout '​default';​
 +% title '​Error';​
 +
 +<​h5>​Oops... Exception</​h5>​
 +
 +<​pre>  ​
 +%= $exception
 +</​pre>​
 +
 +%#EOF
 +</​code>​
 +
 +====templs/​exception.production.html.ep====
 +
 +<code perl templs/​exception.production.html.ep>​
 +%#
 +%# $Id: exception.html.ep 627 2017-04-15 13:02:08Z ziggi $
 +%#
 +% layout '​default';​
 +% title '​Error';​
 +
 +<​h5>​Oops... Exception</​h5>​
 +
 +<pre>
 +%= $exception
 +</​pre>​
 +
 +%#EOF
 +</​code>​
 +
 +====templs/​hello.html.ep====
 +
 +<code perl templs/​hello.html.ep>​
 +% layout '​default';​
 +% title '​Bird';​
 +
 +% use Mojo::Util qw(dumper html_unescape unquote);
 +
 +<h5 class="​text-center">​Hi! How are you there?</​h5>​
 +
 +%#EOF
 +
 +</​code>​
 +
 +====templs/​login.html.ep====
 +
 +<code perl templs/​login.html.ep>​
 +%#
 +%# $Id: login.html.ep 634 2017-04-15 13:55:49Z ziggi $
 +%#
 +<html class="​no-js"​ lang="​en"​ dir="​ltr">​
 +    <​head>​
 +        <meta charset="​utf-8">​
 +        <meta http-equiv="​x-ua-compatible"​ content="​ie=edge">​
 +        <meta name="​viewport"​ content="​width=device-width,​ initial-scale=1.0">​
 +        <​title>​Login</​title>​
 +        <link rel="​stylesheet"​ href="/​css/​foundation-float.min.css">​
 +        <link rel="​stylesheet"​ href="/​css/​app.css">​
 +
 +        <link rel="​stylesheet"​ href="/​icons/​foundation-icons.css">​
 +
 +        <script src="/​js/​jquery.min.js"></​script>​
 +        <script src="/​js/​foundation.min.js"></​script>​
 +    </​head>​
 +    <​body>​
 +
 +        <div class="​row">&​nbsp;</​div>​
 +        <div class="​row">​
 +            <div class="​small-3 columns hide-for-small">&​nbsp;</​div>​
 +            <div class="​small-6 columns text-center">​
 +                <div class="​row">​
 +                    <div class="​columns">​
 +
 +                        <form accept-charset="​UTF-8"​ method="​post"​ action="/​login">​
 +                            <div class="​row column">​
 +                                <h4 class="​text-center">​Login with your username</​h4>​
 +                                <​label>​Username
 +                                    <input type="​text"​ name="​username"​ placeholder="​username"​ />
 +                                </​label>​
 +                                <​label>​Password
 +                                    <input type="​password"​ name="​password"​ placeholder="​password"​ />
 +                                </​label>​
 +                                <p>
 +                                    <button type="​submit"​ class="​button">​Log In</​button>​
 +                                </p>
 +                                <p class="​text-center"></​p>​
 +                            </​div>​
 +                        </​form>​
 +
 +                    </​div>​
 +                </​div>​
 +            </​div>​
 +            <div class="​small-3 columns hide-for-small">&​nbsp;</​div>​
 +        </​div>​
 +
 +        <hr/>
 +        <div class="​row">​
 +            <p class="​text-center"><​small>​Made by <a href="​http://​wiki.unix7.org">​Borodin Oleg</​a></​small></​p>​
 +        </​div>​
 +
 +        <script src="/​js/​app.js"></​script>​
 +    </​body>​
 +</​html>​
 +<!- EOF ->
 +%# EOF
 +</​code>​
 +
 +====templs/​not_found.development.html.ep====
 +
 +<code perl templs/​not_found.development.html.ep>​
 +%#
 +%# $Id: not_found.html.ep 627 2017-04-15 13:02:08Z ziggi $
 +%#
 +% layout '​default';​
 +% title '404 Not found';​
 +
 +<​h5>​404 Page not found</​h5>​
 +
 +%#EOF
 +</​code>​
 +
 +====templs/​not_found.production.html.ep====
 +
 +<code perl templs/​not_found.production.html.ep>​
 +%#
 +%# $Id: not_found.html.ep 627 2017-04-15 13:02:08Z ziggi $
 +%#
 +% layout '​default';​
 +% title '404 Not found';​
 +
 +<​h5>​404 Page not found</​h5>​
 +
 +%#EOF
 +</​code>​
 +
 +====templs/​vpn-all.html.ep====
 +
 +<code perl templs/​vpn-all.html.ep>​
 +%#
 +%# $Id$
 +%#
 +% layout '​default';​
 +% title 'VPN Sw';
 +
 +% use Mojo::Util qw(dumper);
 +
 +<div class="​button"​ id="​start">​Start All</​div>​
 +<div class="​button"​ id="​stop">​Stop All</​div>​
 +
 +<p>
 +Please, don't close the page until all proceses are completed<​br/>​
 +Proszę nie zamykać strony, aż wszystkie procesy są zakończone<​br/>​
 +Будь ласка, не закривайте сторінку до завершення всіх процесів<​br/>​
 +すべてのプロセスが完了するまでページを閉じないでください<​br/>​
 +</p>
 +
 +<div id="​log"​ class="​callout"></​div>​
 +
 +% my $confdir = $self->​app->​config('​confdir'​);​
 +% my $vpn = $self->​app->​vpn;​
 +% my $conf_list = $vpn->​conf_list($confdir);​
 +
 +% my $str;
 +% my $count = 0;
 +
 +% foreach my $conffile (sort @{$conf_list}) {
 +    % my $name = $vpn->​conf_basename($conffile);​
 +    % $str = $str.",​ '​$name'";​
 +    % $count += 1;
 +% }
 +% $str =~ s/^,//;
 +
 +<​script>​
 +
 +function start_all() {
 +    var int = 1100;
 +    var count = <%== $count %>;
 +
 +    $("#​log"​).empty();​
 +    function doSetTimeout(n,​ service) {
 +        setTimeout(function() {
 +            let num = n + 1;
 +
 +            $("#​log"​).append(num + '/'​ + count + ": Start service ​ " + service + " ... ");
 +            $.ajax({
 +                dataType: '​json',​
 +                url: '/​j/​vpn/​start?​service='​ + service,
 +                success: function(data) {
 +                    $("#​log"​).append(data.status + '<​br/>'​);​
 +                    $('​html,​body'​).animate({
 +                                scrollTop: document.body.scrollHeight},​
 +                                "​slow"​
 +                    );
 +                }
 +            });
 +
 +        },
 +        n * int + int/2 + 100);
 +    }
 +
 +    var list = [ <%== $str %>];
 +
 +    for (var i = 0; i < list.length;​ i++) {
 +        doSetTimeout(i,​ list[i]);
 +    }
 +}
 +
 +function stop_all() {
 +    var int = 1100;
 +    var count = <%== $count %>;
 +
 +    $("#​log"​).empty();​
 +    function doSetTimeout(n,​ service) {
 +        setTimeout(function() {
 +            let num = n + 1;
 +
 +            $("#​log"​).append(num + '/'​ + count + ": Stop service ​ " + service + " ... ");
 +            $.ajax({
 +                dataType: '​json',​
 +                url: '/​j/​vpn/​stop?​service='​ + service,
 +                success: function(data) {
 +                    $("#​log"​).append(data.status + '<​br/>'​);​
 +                    $('​html,​body'​).animate({
 +                                scrollTop: document.body.scrollHeight},​
 +                                "​slow"​
 +                    );
 +                }
 +            });
 +
 +        },
 +        n * int + int/2 + 100);
 +    }
 +
 +    var list = [ <%== $str %>];
 +
 +    for (var i = 0; i < list.length;​ i++) {
 +        doSetTimeout(i,​ list[i]);
 +    }
 +}
 +
 +$("#​start"​).dblclick(function() {
 +    start_all();​
 +});
 +
 +$("#​stop"​).dblclick(function() {
 +    stop_all();
 +});
 +
 +</​script>​
 +
 +%#EOF
 +
 +</​code>​
 +
 +====templs/​vpn-info.html.ep====
 +
 +<code perl templs/​vpn-info.html.ep>​
 +%#
 +%# $Id$
 +%#
 +% layout '​default';​
 +% title 'VPN Sw';
 +% use Mojo::Util qw(dumper);
 +% use File::​Basename;​
 +% use File::stat;
 +% use POSIX;
 +
 +% my $req = $c->req;
 +% my $confdir = $self->​app->​config('​confdir'​);​
 +% my $vpn = $self->​app->​vpn;​
 +
 +<div class="​text-center">​
 +    <​h5>​Network details <a href="/​vpn/​info"><​i class="​fi-refresh""></​i></​a></​h5>​
 +</​div>​
 +
 +% my $conf_list = $vpn->​conf_list($confdir);​
 +
 +<table id="​table"​ class="​table-scroll hover">​
 +    <​thead>​
 +        <tr>
 +            <​th>#</​th>​
 +            <​th>​cn</​th>​
 +            <​th>​public</​th>​
 +            <​th>​service</​th>​
 +            <​th>​stat</​th>​
 +            <​th>​vpn net</​th>​
 +            <​th>​tun addr</​th>​
 +            <​th>​off addr</​th>​
 +            <​th>​wrk addr</​th>​
 +        </tr>
 +    </​thead>​
 +    <​tbody>​
 +% my $num = 1;
 +% foreach my $conffile (sort @{$conf_list}) {
 +
 +    % my $name = $vpn->​conf_basename($conffile);​
 +    % my $conf = $vpn->​conf_parse($conffile);​
 +
 +    % my $server_net = $conf->​{'​server'​} || '';​
 +
 +    % my $statfile = $conf->​{'​status'​} || '';​
 +    % my $stat = $vpn->​stat_parse($statfile) || undef;
 +    % my $conn = scalar keys %{$stat};
 +
 +    % my $netcount = 0;
 +    % foreach my $cn (keys %{$stat}) {
 +        % $netcount++ if $stat->​{$cn}{'​net'​}
 +    % }
 +
 +    % my $status = $vpn->​service_status($name) || '';​
 +
 +    % foreach my $cn (sort keys %{$stat}) {
 +        % my $ipaddr = $stat->​{$cn}{'​ipaddr'​} || '';​
 +        % my $date = $stat->​{$cn}{'​date'​} || '';​
 +        % my $peer = $stat->​{$cn}{'​local'​} || '';​
 +        % my $nets = $stat->​{$cn}{'​net'​} || undef ;
 +        % my $netstr;
 +        % foreach my $net (@{$nets}) {
 +            % $netstr .= "$net ";
 +        % }
 +        %  $netstr ||= '​ws/​nat';​
 +
 +        % my $wclients = $stat->​{$cn}{'​wclient'​} || () ;
 +        % my $wcstr = '';​
 +        % foreach my $wc (@{$wclients}) {
 +            % $wcstr .= "$wc ";
 +        % }
 +        <tr>
 +            <​td><​%= $num %></​td>​
 +            <​td><​%= $cn %></​td>​
 +            <​td><​%= $ipaddr %></​td>​
 +            <​td><​%= $name %></​td> ​
 +            <​td><​%= $status %></​td>​
 +            <​td><​%= $vpn->​pack_ipaddr($server_net) if $server_net %></​td>​
 +            <​td><​%= $peer %></​td>​
 +            <​td><​%= $netstr %></​td>​
 +            <​td><​%= $wcstr %></​td>​
 +        </tr>
 +        % $num++;
 +    % }
 +% };
 +</​table>​
 +
 +<​script>​
 +    $.extend(true,​ $.fn.dataTable.defaults,​ {
 +        "​searching":​ true,
 +        "​ordering":​ true,
 +        "​pageLength":​ -1,
 +        "​lengthMenu":​ [ [10, 25, 50,100, -1], [10, 25, 50,100, "​All"​] ],
 +        "​language":​ {
 +            "​search":​ "",​
 +            "​lengthMenu":​ "​_MENU_",​
 +            "​info":​ "​_START_-_END_ of _TOTAL_",​
 +            "​infoEmpty":​ "",​
 +        },
 +    });
 +
 +    $(document).ready(function() {
 +        $('#​table'​).DataTable();​
 +    });
 +</​script>​
 +%#EOF
 +
 +</​code>​
 +
 +====templs/​vpn-list.html.ep====
 +
 +<code perl templs/​vpn-list.html.ep>​
 +%#
 +%# $Id$
 +%#
 +% layout '​default';​
 +% title 'VPN Sw';
 +
 +% use Mojo::Util qw(dumper);
 +% use File::​Basename;​
 +% use File::stat;
 +% use POSIX;
 +
 +% my $req = $c->req;
 +% my $confdir = $self->​app->​config('​confdir'​);​
 +% my $vpn = $self->​app->​vpn;​
 +
 +% my $request = $req->​param('​request'​) || '';​
 +% my $service = $req->​param('​service'​) || undef;
 +
 +% $vpn->​service_start($service) if ($request eq '​start'​ && defined $service);
 +% $vpn->​service_stop($service) if ($request eq '​stop'​ && defined $service);
 +
 +<div class="​text-center">​
 +    <​h5>​VPN services <a href="/​vpn/​list"><​i class="​fi-refresh"></​i></​a></​h5>​
 +</​div> ​
 +
 +% my $conf_list = $vpn->​conf_list($confdir);​
 +
 +% foreach my $conffile (sort @{$conf_list}) {
 +    % my $name = $vpn->​conf_basename($conffile);​
 +
 +    % my $conf = $vpn->​conf_parse($conffile);​
 +    % my $statfile = $conf->​{'​status'​} || '';​
 +    % my $stat = $vpn->​stat_parse($statfile) || undef;
 +
 +    % my $status = $vpn->​service_status($name) || '';​
 +
 +    % my $action = '​stop';​
 +    % $action = '​start'​ if $status eq '​down';​
 +
 +%#    <div class="​reveal"​ id="​modal-action-<​%= $name %>" data-reveal>​
 +%#        <div class="​text-center">​
 +%#            <​h5>​Do switch status?</​h5>​
 +%#        </​div>​
 +%#        <form accept-charset="​UTF-8"​ action="/​vpn/​list"​ method="​get">​
 +%#            <input type="​hidden"​ name="​request"​ value="<​%= $action %>" />
 +%#            <input type="​hidden"​ name="​service"​ value="<​%= $name %>" />
 +%#
 +%#            <p class="​text-center">​
 +%#                <button type="​submit"​ class="​button alert">​Yes,​ I agree</​button>​
 +%#                <button class="​button"​ data-close="​modal-action-<​%= $name %>" type="​button">​No,​ Escape</​button>​
 +%#            </p>
 +%#        </​form>​
 +%#        <button class="​close-button"​ data-close="​modal-action-<​%= $name %>" type="​button">&​times;</​button>​
 +%#    </​div>​
 +
 +    <div class="​reveal large" id="​modal-<​%= $name %>" data-reveal>​
 +        <div>
 +            <​h5 ​ class="​text-center">​Service <%= $name %> </h5>
 +
 +    % if ($stat) {
 +        % my $subnum = 1;
 +            <table class="​table-scroll">​
 +                    <​thead>​
 +                        <tr>
 +                            <​th>​cn</​th>​
 +                            <​th>​ipaddr</​th>​
 +                            <​th>​start</​th>​
 +                            <​th>​peer</​th>​
 +                            <​th>​remote</​th>​
 +                            <​th>​wc</​th>​
 +                        </tr>
 +                </​thead>​
 +
 +        % foreach my $cn (sort keys %{$stat}) {
 +            % my $ipaddr = $stat->​{$cn}{'​ipaddr'​} || '';​
 +            % my $date = $stat->​{$cn}{'​date'​} || '';​
 +            % my $peer = $stat->​{$cn}{'​local'​} || '';​
 +            % my $net = $stat->​{$cn}{'​net'​} || 'ws '​.$peer ;
 +
 +            % my $nets = $stat->​{$cn}{'​net'​} || undef ;
 +            % my $netstr;
 +            %  foreach my $net (@{$nets}) {
 +            %          $netstr .= "$net ";
 +            % }
 +            % $netstr ||= '​ws/​nat';​
 +
 +            % my $wclients = $stat->​{$cn}{'​wclient'​} ;
 +            % my $wcstr = '';​
 +            % foreach my $wc (@{$wclients}) {
 +            %      $wcstr .= "$wc ";
 +            % }
 +
 +                    <tr>
 +                        <​td><​%= $cn %></​td>​
 +                        <​td><​%= $ipaddr %></​td>​
 +                        <​td><​%= $date %></​td>​
 +                        <​td><​%= $peer %></​td>​
 +                        <​td><​%= $netstr %></​td>​
 +                        <​td><​%= $wcstr %></​td>​
 +                    </tr>
 +            % $subnum++;
 +        % }
 +            </​table>​
 +
 +    % }
 +        </​div>​
 +        <button class="​close-button"​ data-close="​modal-<​%= $name %>" type="​button">&​times;</​button>​
 +    </​div>​
 +% }
 +
 +% my $total_tun = 0;
 +% my $total_tun_up = 0;
 +% my $total_net = 0;
 +% my $total_ws = 0;
 +% my $totalConn = 0;
 +
 +% foreach my $conffile (sort @{$conf_list}) {
 +    % my $name = $vpn->​conf_basename($conffile);​
 +    % my $conf = $vpn->​conf_parse($conffile);​
 +
 +    % my $server = $conf->​{'​server'​} || '';​
 +
 +    % my $statfile = $conf->​{'​status'​} || '';​
 +    % my $stat = $vpn->​stat_parse($statfile) || undef;
 +    % $total_tun = $total_tun + scalar (keys %{$stat});
 +
 +    % foreach my $cn (keys %{$stat}) {
 +        % $total_net++ if $stat->​{$cn}{'​net'​};​
 +        % $total_ws++ unless $stat->​{$cn}{'​net'​};​
 +    % }
 +
 +    % foreach my $cn (keys %{$stat}) {
 +        % $total_ws = $total_ws + scalar @{$stat->​{$cn}{'​wclient'​}} if $stat->​{$cn}{'​wclient'​};​
 +    % }
 +
 +    % my $status = $vpn->​service_status($name) || '';​
 +    % my $request = '​start';​
 +    % $request = '​stop'​ if $status eq '​up'; ​
 +
 +% }
 +
 +<p class="​text-center">​
 +total tun:<%= $total_tun %> 
 +total ws:<%= $total_ws %>
 +</p>
 +
 +
 +<table id="​table"​ class="​table-scroll"​ >
 +    <​thead>​
 +        <tr>
 +            <​th>#</​th>​
 +            <​th>​service</​th>​
 +            <​th>​status</​th>​
 +            <​th>#​tun</​th>​
 +            <​th>#​net</​th>​
 +            <​th>#​wc</​th>​
 +        </tr>
 +    </​thead>​
 +    <​tbody>​
 +
 +        % my $num = 1;
 +        % foreach my $conffile (sort @{$conf_list}) {
 +
 +        % my $name = $vpn->​conf_basename($conffile);​
 +        % my $conf = $vpn->​conf_parse($conffile);​
 +        % my $server = $conf->​{'​server'​} || '';​
 +
 +        % my $statfile = $conf->​{'​status'​} || '';​
 +        % my $stat = $vpn->​stat_parse($statfile) || undef;
 +        % my $conn = scalar keys %{$stat};
 +
 +        % my $net_count = 0;
 +        % my $wc_count = 0;
 +        % foreach my $cn (keys %{$stat}) {
 +            % $net_count++ if $stat->​{$cn}{'​net'​};​
 +            % $wc_count++ unless $stat->​{$cn}{'​net'​};​
 +        % }
 +
 +        % foreach my $cn (keys %{$stat}) {
 +            % $wc_count = $wc_count + @{$stat->​{$cn}{'​wclient'​}} if $stat->​{$cn}{'​wclient'​};​
 +        % }
 +
 +        % my $status = $vpn->​service_status($name) || '';​
 +        % my $request = '​start';​
 +        % $request = '​stop'​ if $status eq '​up'; ​
 +
 +        % my $stat_icon = '​fi-x';​
 +        % $stat_icon = '​fi-play'​ if $status eq '​up';​
 +
 +        % my $stat_color = '​alert';​
 +        % $stat_color = '​success'​ if $status eq '​up';​
 +        <tr>
 +            <​td><​%= $num %></​td>​
 +            <​td><​%= $name %></​td> ​
 +            <​td><​a id="​status_<​%= $name %>"><​%= $status %> <i class="<​%= $stat_icon %>"></​i></​a></​td>​
 +            <​td><​a href="#"​ data-open="​modal-<​%= $name %>"><​%= $conn if $conn %></​a></​td>​
 +            <​td><​%= $net_count if $net_count %></​td>​
 +            <​td><​%= $wc_count if $wc_count %></​td>​
 +        </tr>
 +        % $num++;
 +% }
 +</​table>​
 +
 +% foreach my $conffile (sort @{$conf_list}) {
 +    % my $name = $vpn->​conf_basename($conffile);​
 +    % my $status = $vpn->​service_status($name) || '';​
 +
 +    <​script>​
 +        var status_<​%= $name %> = "<​%= $status %>";​
 +
 +        $("#​status_<​%= $name %>"​).dblclick(function() {
 +            if (status_<​%= $name %> == '​down'​) {
 +                $.ajax({
 +                    dataType: '​json',​
 +                    url: '/​j/​vpn/​start?​service=<​%= $name %>',​
 +                    success: function(data) {
 +                        if (data.status == '​up'​) {
 +                            $("#​status_<​%= $name %>"​).html('​up <i class="​fi-play"></​i>'​);​
 +                            status_<​%= $name %> = '​up';​
 +                        }
 +                    }
 +                });
 +            } else {
 +                $.ajax({
 +                    dataType: '​json',​
 +                    url: '/​j/​vpn/​stop?​service=<​%= $name %>',​
 +                    success: function(data) {
 +                        if (data.status == '​down'​) {
 +                            $("#​status_<​%= $name %>"​).html('​down <i class="​fi-x"></​i>'​);​
 +                            status_<​%= $name %> = '​down';​
 +                        }
 +                    }
 +                });
 +            }
 +        });
 +    </​script>​
 +% }
 +
 +<​script>​
 +    $.extend(true,​ $.fn.dataTable.defaults,​ {
 +        "​searching":​ true,
 +        "​ordering":​ true,
 +        "​pageLength":​ -1,
 +        "​lengthMenu":​ [ [10, 25, 50,100, -1], [10, 25, 50,100, "​All"​] ],
 +        "​language":​ {
 +            "​search":​ "",​
 +            "​lengthMenu":​ "​_MENU_",​
 +            "​info":​ "​_START_-_END_ of _TOTAL_",​
 +            "​infoEmpty":​ "",​
 +        },
 +    });
 +
 +    $(document).ready(function() {
 +        $('#​table'​).DataTable();​
 +    });
 +</​script>​
 +%#EOF
 +
 +</​code>​
 +
 +
 +----
 +
 + --- //​[[onborodin@gmail.com|Borodin Oleg]] 2017/12/13 13:18//
 +
 +[<>]
 +
 +