Initial commit.
This commit is contained in:
commit
93b5bf300a
28
Build.PL
Executable file
28
Build.PL
Executable file
@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env perl
|
||||||
|
use Module::Build;
|
||||||
|
|
||||||
|
my $home = $ENV{HOME};
|
||||||
|
|
||||||
|
my $build = Module::Build->new(
|
||||||
|
module_name => 'VPNManager',
|
||||||
|
license => 'AGPLv3',
|
||||||
|
dist_author => 'Sergio Iglesias <contact@owlcode.tech>',
|
||||||
|
dist_abstract => 'Manage Wireguard.',
|
||||||
|
requires => {
|
||||||
|
'DBI' => 0,
|
||||||
|
'DBD::SQLite' => 0,
|
||||||
|
'Path::Tiny' => 0,
|
||||||
|
'DBIx::Class' => 0,
|
||||||
|
'Mojolicious' => 0,
|
||||||
|
'Moo' => 0,
|
||||||
|
'Crypt::Bcrypt' => 0,
|
||||||
|
'Crypt::URandom' => 0,
|
||||||
|
'Capture::Tiny' => 0,
|
||||||
|
},
|
||||||
|
test_requires => {
|
||||||
|
'Test::MockModule' => 0,
|
||||||
|
'Test::Most' => 0,
|
||||||
|
'Test::MockObject' => 0,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
$build->create_build_script;
|
70
lib/VPNManager.pm
Normal file
70
lib/VPNManager.pm
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
package VPNManager;
|
||||||
|
|
||||||
|
use v5.38.2;
|
||||||
|
|
||||||
|
use strict;
|
||||||
|
use warnings;
|
||||||
|
|
||||||
|
use Mojo::Base 'Mojolicious', -signatures;
|
||||||
|
|
||||||
|
use Path::Tiny;
|
||||||
|
|
||||||
|
# This method will run once at server start
|
||||||
|
sub startup ($self) {
|
||||||
|
|
||||||
|
# Load configuration from config file
|
||||||
|
system 'chmod', '600', path(__FILE__)->parent->parent->child('v_p_n_manager.yml');
|
||||||
|
my $config = $self->plugin('NotYAMLConfig');
|
||||||
|
|
||||||
|
# Configure the application
|
||||||
|
$self->secrets( $config->{secrets} );
|
||||||
|
|
||||||
|
# Router
|
||||||
|
my $r = $self->routes;
|
||||||
|
|
||||||
|
# Normal route to controller
|
||||||
|
my $routes = $r->under(
|
||||||
|
'/',
|
||||||
|
sub {
|
||||||
|
my $c = shift;
|
||||||
|
my $redirect_login = sub {
|
||||||
|
my $c = shift;
|
||||||
|
my $url = Mojo::URL->new('/login');
|
||||||
|
$url->query( redirect_to => $c->url_for );
|
||||||
|
$c->redirect_to($url);
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
if ( $c->url_for->path =~ /^\/login\/?$/ ) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if ( !defined $c->session->{user} ) {
|
||||||
|
return $redirect_login->($c);
|
||||||
|
}
|
||||||
|
require VPNManager::Schema;
|
||||||
|
my $schema = VPNManager::Schema->Schema;
|
||||||
|
my $resultset_admins = $schema->resultset('Admin');
|
||||||
|
my ($user) = $resultset_admins->search(
|
||||||
|
{
|
||||||
|
username => $c->session->{user},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if ( !defined $user ) {
|
||||||
|
delete $c->session->{user};
|
||||||
|
return $redirect_login->($c);
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
$routes->get('/')->to('Main#main');
|
||||||
|
$routes->get('/login')->to('Main#login');
|
||||||
|
$routes->post('/login')->to('Main#login_post');
|
||||||
|
$routes->get('/vpn/create-user')->to('Main#create_vpn_user');
|
||||||
|
$routes->post('/vpn/create-user')->to('Main#create_vpn_user_post');
|
||||||
|
$routes->get('/vpn/user/:id/details')->to('Main#show_user_details');
|
||||||
|
$routes->post('/vpn/user/:id/download')->to('Main#download_file');
|
||||||
|
$routes->post('/vpn/user/:id/enable')->to('Main#enable_user');
|
||||||
|
$routes->post('/vpn/user/:id/disable')->to('Main#disable_user');
|
||||||
|
# $routes->post('/vpn/save')->to('Main#save_vpn_settings');
|
||||||
|
}
|
||||||
|
1;
|
11
lib/VPNManager/Controller/Example.pm
Normal file
11
lib/VPNManager/Controller/Example.pm
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package VPNManager::Controller::Example;
|
||||||
|
use Mojo::Base 'Mojolicious::Controller', -signatures;
|
||||||
|
|
||||||
|
# This action will render a template
|
||||||
|
sub welcome ($self) {
|
||||||
|
|
||||||
|
# Render template "example/welcome.html.ep" with message
|
||||||
|
$self->render(msg => 'Welcome to the Mojolicious real-time web framework!');
|
||||||
|
}
|
||||||
|
|
||||||
|
1;
|
188
lib/VPNManager/Controller/Main.pm
Normal file
188
lib/VPNManager/Controller/Main.pm
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
package VPNManager::Controller::Main;
|
||||||
|
|
||||||
|
use v5.38.2;
|
||||||
|
|
||||||
|
use strict;
|
||||||
|
use warnings;
|
||||||
|
|
||||||
|
use Crypt::Bcrypt qw/bcrypt_check/;
|
||||||
|
use VPNManager::Schema;
|
||||||
|
use Capture::Tiny qw/capture_stdout/;
|
||||||
|
|
||||||
|
use Mojo::Base 'Mojolicious::Controller', -signatures;
|
||||||
|
use Path::Tiny;
|
||||||
|
|
||||||
|
sub main($self) {
|
||||||
|
my $resultset = VPNManager::Schema->Schema->resultset('VPNUser');
|
||||||
|
my @users = $resultset->search( {} );
|
||||||
|
$self->stash( users => \@users );
|
||||||
|
$self->render( template => 'main/index' );
|
||||||
|
}
|
||||||
|
|
||||||
|
sub login($self) {
|
||||||
|
$self->render( template => 'main/login' );
|
||||||
|
}
|
||||||
|
|
||||||
|
sub login_post($self) {
|
||||||
|
my $password = $self->param('password');
|
||||||
|
my $username = $self->param('username');
|
||||||
|
my ($user) = VPNManager::Schema->Schema->resultset('Admin')->search(
|
||||||
|
{
|
||||||
|
username => $username
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if ( !defined $user ) {
|
||||||
|
return $self->_invalid_login;
|
||||||
|
}
|
||||||
|
if ( !bcrypt_check $password, $user->password ) {
|
||||||
|
return $self->_invalid_login;
|
||||||
|
}
|
||||||
|
$self->session->{user} = $username;
|
||||||
|
return $self->redirect_to('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
sub _invalid_login($self) {
|
||||||
|
$self->render( text => 'Invalid login', status => 401 );
|
||||||
|
}
|
||||||
|
|
||||||
|
sub create_vpn_user($self) {
|
||||||
|
$self->render( template => 'vpn/create-user' );
|
||||||
|
}
|
||||||
|
|
||||||
|
sub create_vpn_user_post($self) {
|
||||||
|
my $name = $self->param('name');
|
||||||
|
my $config = $self->config;
|
||||||
|
my $starting_ip = $config->{vpnclients}{starting_ip};
|
||||||
|
my $resultset = VPNManager::Schema->Schema->resultset('VPNUser');
|
||||||
|
my ($last_ip_user) = $resultset->search(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
order_by => { -desc => 'ip' },
|
||||||
|
rows => 1,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
my $ip = $starting_ip;
|
||||||
|
my $there_is_previous_user = 0;
|
||||||
|
if ( defined $last_ip_user ) {
|
||||||
|
$ip = $last_ip_user->ip_to_text;
|
||||||
|
$there_is_previous_user = 1;
|
||||||
|
}
|
||||||
|
my $new_user = $resultset->new(
|
||||||
|
{ name => $name, publickey => '', ip => '', is_enabled => 0 } );
|
||||||
|
$new_user->ip_from_text($ip);
|
||||||
|
$new_user->ip( $new_user->ip + 1 ) if $there_is_previous_user;
|
||||||
|
$ip = $new_user->ip_to_text;
|
||||||
|
$new_user->insert;
|
||||||
|
$new_user = $new_user->get_from_storage;
|
||||||
|
my $id = $new_user->id;
|
||||||
|
my $url = Mojo::URL->new("/vpn/user/$id/details");
|
||||||
|
return $self->redirect_to($url);
|
||||||
|
}
|
||||||
|
|
||||||
|
sub download_file($self) {
|
||||||
|
my $id = $self->param('id');
|
||||||
|
my $resultset = VPNManager::Schema->Schema->resultset('VPNUser');
|
||||||
|
my ($user) = $resultset->search( { id => $id } );
|
||||||
|
if ( !defined $user ) {
|
||||||
|
return $self->render( text => 'No such user', status => 400 );
|
||||||
|
}
|
||||||
|
my $private_key = `wg genkey`;
|
||||||
|
my $public_key = capture_stdout sub {
|
||||||
|
open my $fh, '|-', 'wg', 'pubkey';
|
||||||
|
print $fh $private_key;
|
||||||
|
};
|
||||||
|
chomp $private_key;
|
||||||
|
$user->update( { publickey => $public_key } );
|
||||||
|
my $config = $self->config;
|
||||||
|
my $ip = $user->ip_to_text;
|
||||||
|
my $vpn_file = <<"EOF";
|
||||||
|
[Interface]
|
||||||
|
PrivateKey = $private_key
|
||||||
|
Address = $ip/32
|
||||||
|
DNS = @{[$config->{vpn}{host}]}
|
||||||
|
|
||||||
|
[Peer]
|
||||||
|
PublicKey = @{[$config->{vpn}{privkey}]}
|
||||||
|
AllowedIPs = @{[$config->{vpnclients}{allowedips}]}
|
||||||
|
Endpoint = @{[$config->{vpnclients}{endpoint}]}:@{[$config->{vpnclients}{server_port}]}
|
||||||
|
EOF
|
||||||
|
my $filename = $user->name . '-vpn.conf';
|
||||||
|
$self->res->headers->add( 'Content-Type',
|
||||||
|
'application/x-download;name=' . $filename );
|
||||||
|
$self->res->headers->add( 'Content-Disposition',
|
||||||
|
'attachment;filename=' . $filename );
|
||||||
|
$self->render( data => $vpn_file );
|
||||||
|
}
|
||||||
|
|
||||||
|
sub show_user_details($self) {
|
||||||
|
my $id = $self->param('id');
|
||||||
|
my $resultset = VPNManager::Schema->Schema->resultset('VPNUser');
|
||||||
|
my ($user) = $resultset->search( { id => $id } );
|
||||||
|
if ( !defined $user ) {
|
||||||
|
return $self->render( text => 'No such user', status => 400 );
|
||||||
|
}
|
||||||
|
$self->stash( details_user => $user );
|
||||||
|
return $self->render( template => 'vpn/user-details' );
|
||||||
|
}
|
||||||
|
|
||||||
|
sub enable_user($self) {
|
||||||
|
my $id = $self->param('id');
|
||||||
|
my $resultset = VPNManager::Schema->Schema->resultset('VPNUser');
|
||||||
|
my ($user) = $resultset->search( { id => $id } );
|
||||||
|
if ( !defined $user ) {
|
||||||
|
return $self->render( text => 'No such user', status => 400 );
|
||||||
|
}
|
||||||
|
if ( $user->publickey eq '' ) {
|
||||||
|
return $self->render(
|
||||||
|
text => 'You must first download the vpn file',
|
||||||
|
status => 400
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$user->update( { is_enabled => 1 } );
|
||||||
|
return $self->redirect_to('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
sub disable_user($self) {
|
||||||
|
my $id = $self->param('id');
|
||||||
|
my $resultset = VPNManager::Schema->Schema->resultset('VPNUser');
|
||||||
|
my ($user) = $resultset->search( { id => $id } );
|
||||||
|
if ( !defined $user ) {
|
||||||
|
return $self->render( text => 'No such user', status => 400 );
|
||||||
|
}
|
||||||
|
return $self->render( text => 'This user is protected', status => 400 )
|
||||||
|
if $user->is_protected;
|
||||||
|
$user->update( { is_enabled => 0 } );
|
||||||
|
return $self->redirect_to('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
#sub save_vpn_settings($self) {
|
||||||
|
# my $out_dir = path(__FILE__)->parent->parent->parent->parent->child('out');
|
||||||
|
# $out_dir->mkpath;
|
||||||
|
# system 'chmod', '700', $out_dir;
|
||||||
|
# my $config = $self->config;
|
||||||
|
# my $vpn_config = <<"EOF";
|
||||||
|
#[Interface]
|
||||||
|
#Address = @{[$config->{vpn}{host}]}/@{[$config->{vpn}{submask}]}
|
||||||
|
#MTU = @{[$config->{vpn}{mtu}]}
|
||||||
|
#SaveConfig = false
|
||||||
|
#ListenPort = @{[$config->{vpnclients}{server_port}]}
|
||||||
|
#PrivateKey = @{[$config->{vpn}{privkey}]}
|
||||||
|
#EOF
|
||||||
|
# my $resultset = VPNManager::Schema->Schema->resultset('VPNUser');
|
||||||
|
# my @users = $resultset->search( {} );
|
||||||
|
#
|
||||||
|
# for my $user (@users) {
|
||||||
|
# next if !$user->is_enabled;
|
||||||
|
#
|
||||||
|
# $vpn_config .= <<"EOF";
|
||||||
|
#
|
||||||
|
#[Peer]
|
||||||
|
#PublicKey = @{[$user->publickey]}
|
||||||
|
#AllowedIPs = @{[$user->ip_to_text]}/32
|
||||||
|
#Endpoint = @{[$config->{vpn}{endpoint}]}:@{[$config->{vpnclients}{server_port}]}
|
||||||
|
#EOF
|
||||||
|
# }
|
||||||
|
# $out_dir->child('wg0.conf')->spew_raw($vpn_config);
|
||||||
|
# return $self->redirect_to('/');
|
||||||
|
#}
|
||||||
|
1;
|
116
lib/VPNManager/DB.pm
Normal file
116
lib/VPNManager/DB.pm
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
package VPNManager::DB;
|
||||||
|
|
||||||
|
use v5.38.2;
|
||||||
|
|
||||||
|
use strict;
|
||||||
|
use warnings;
|
||||||
|
|
||||||
|
use feature 'signatures';
|
||||||
|
|
||||||
|
use DBI;
|
||||||
|
use DBD::SQLite;
|
||||||
|
|
||||||
|
use VPNManager::DB::Migrations;
|
||||||
|
use Path::Tiny;
|
||||||
|
use Data::Dumper;
|
||||||
|
|
||||||
|
my $dbh;
|
||||||
|
|
||||||
|
sub reset_dbh {
|
||||||
|
undef $dbh;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub connect {
|
||||||
|
if ( defined $dbh ) {
|
||||||
|
return $dbh;
|
||||||
|
}
|
||||||
|
my $class = shift;
|
||||||
|
require VPNManager;
|
||||||
|
my $app = VPNManager->new;
|
||||||
|
my $config = $app->config;
|
||||||
|
my $db_path = $class->_db_path;
|
||||||
|
$dbh = DBI->connect(
|
||||||
|
'dbi:SQLite:dbname='.$db_path,
|
||||||
|
undef, undef,
|
||||||
|
{
|
||||||
|
RaiseError => 1,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
$class->_migrate($dbh);
|
||||||
|
return $dbh;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub _db_path($class) {
|
||||||
|
my $home = $ENV{HOME};
|
||||||
|
my $db_path = '';
|
||||||
|
{
|
||||||
|
$db_path = $home . '/' if $home;
|
||||||
|
}
|
||||||
|
$db_path .= '.vpnmanager/db.sqlite';
|
||||||
|
path($db_path)->parent->mkpath;
|
||||||
|
system 'chmod', '-v', '700', path($db_path)->parent;
|
||||||
|
return $db_path;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub _migrate {
|
||||||
|
my $class = shift;
|
||||||
|
my $dbh = shift;
|
||||||
|
local $dbh->{RaiseError} = 0;
|
||||||
|
local $dbh->{PrintError} = 0;
|
||||||
|
my @migrations = VPNManager::DB::Migrations::MIGRATIONS();
|
||||||
|
if ( $class->get_current_migration($dbh) > @migrations ) {
|
||||||
|
warn "Something happened there, wrong migration number.";
|
||||||
|
}
|
||||||
|
if ( $class->get_current_migration($dbh) >= @migrations ) {
|
||||||
|
say STDERR "Migrations already applied.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$class->_apply_migrations( $dbh, \@migrations );
|
||||||
|
}
|
||||||
|
|
||||||
|
sub _apply_migrations {
|
||||||
|
my $class = shift;
|
||||||
|
my $dbh = shift;
|
||||||
|
my $migrations = shift;
|
||||||
|
for (
|
||||||
|
my $i = $class->get_current_migration($dbh) ;
|
||||||
|
$i < @$migrations ;
|
||||||
|
$i++
|
||||||
|
)
|
||||||
|
{
|
||||||
|
local $dbh->{RaiseError} = 1;
|
||||||
|
my $current_migration = $migrations->[$i];
|
||||||
|
my $migration_number = $i + 1;
|
||||||
|
$class->_apply_migration( $dbh, $current_migration, $migration_number );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sub _apply_migration {
|
||||||
|
my $class = shift;
|
||||||
|
my $dbh = shift;
|
||||||
|
my $current_migration = shift;
|
||||||
|
my $migration_number = shift;
|
||||||
|
{
|
||||||
|
if (ref $current_migration eq 'CODE') {
|
||||||
|
$current_migration->($dbh);
|
||||||
|
next;
|
||||||
|
}
|
||||||
|
$dbh->do($current_migration);
|
||||||
|
}
|
||||||
|
$dbh->do( <<'EOF', undef, 'current_migration', $migration_number );
|
||||||
|
INSERT INTO options
|
||||||
|
VALUES ($1, $2)
|
||||||
|
ON CONFLICT (name) DO
|
||||||
|
UPDATE SET value = $2;
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
sub get_current_migration {
|
||||||
|
my $class = shift;
|
||||||
|
my $dbh = shift;
|
||||||
|
my $result = $dbh->selectrow_hashref( <<'EOF', undef, 'current_migration' );
|
||||||
|
select value from options where name = ?;
|
||||||
|
EOF
|
||||||
|
return int( $result->{value} // 0 );
|
||||||
|
}
|
||||||
|
1;
|
33
lib/VPNManager/DB/Migrations.pm
Normal file
33
lib/VPNManager/DB/Migrations.pm
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package VPNManager::DB::Migrations;
|
||||||
|
|
||||||
|
use v5.34.1;
|
||||||
|
|
||||||
|
use strict;
|
||||||
|
use warnings;
|
||||||
|
use utf8;
|
||||||
|
|
||||||
|
use feature 'signatures';
|
||||||
|
|
||||||
|
sub MIGRATIONS {
|
||||||
|
return (
|
||||||
|
'CREATE TABLE options (
|
||||||
|
name TEXT PRIMARY KEY,
|
||||||
|
value TEXT
|
||||||
|
);',
|
||||||
|
'CREATE TABLE vpn_users (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
publickey TEXT NOT NULL,
|
||||||
|
is_enabled INTEGER NOT NULL DEFAULT true,
|
||||||
|
is_protected INTEGER NOT NULL DEFAULT true,
|
||||||
|
is_deleted INTEGER NOT NULL DEFAULT false,
|
||||||
|
ip INTEGER NOT NULL
|
||||||
|
);',
|
||||||
|
'CREATE TABLE admins (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
username TEXT NOT NULL UNIQUE,
|
||||||
|
password TEXT NOT NULL
|
||||||
|
);',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
1;
|
36
lib/VPNManager/Schema.pm
Normal file
36
lib/VPNManager/Schema.pm
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
package VPNManager::Schema;
|
||||||
|
|
||||||
|
use v5.36.0;
|
||||||
|
|
||||||
|
use strict;
|
||||||
|
use warnings;
|
||||||
|
|
||||||
|
use feature 'signatures';
|
||||||
|
|
||||||
|
use parent 'DBIx::Class::Schema';
|
||||||
|
|
||||||
|
__PACKAGE__->load_namespaces();
|
||||||
|
|
||||||
|
my $schema;
|
||||||
|
|
||||||
|
sub Schema ($class) {
|
||||||
|
if ( !defined $schema ) {
|
||||||
|
require VPNManager::DB;
|
||||||
|
VPNManager::DB->connect;
|
||||||
|
my $db_path = VPNManager::DB->_db_path;
|
||||||
|
my $user = undef;
|
||||||
|
my $password = undef;
|
||||||
|
# Undef is perfectly fine for username and password.
|
||||||
|
$schema = $class->connect(
|
||||||
|
'dbi:SQLite:dbname='.$db_path, $user, $password,
|
||||||
|
{
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return $schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub reset_schema {
|
||||||
|
undef $schema;
|
||||||
|
}
|
||||||
|
1;
|
29
lib/VPNManager/Schema/Result/Admin.pm
Normal file
29
lib/VPNManager/Schema/Result/Admin.pm
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
package VPNManager::Schema::Result::Admin;
|
||||||
|
|
||||||
|
use v5.38.2;
|
||||||
|
|
||||||
|
use strict;
|
||||||
|
use warnings;
|
||||||
|
|
||||||
|
use feature 'signatures';
|
||||||
|
|
||||||
|
use parent 'DBIx::Class::Core';
|
||||||
|
|
||||||
|
__PACKAGE__->table('admins');
|
||||||
|
|
||||||
|
__PACKAGE__->add_columns(
|
||||||
|
id => {
|
||||||
|
data_type => 'INTEGER',
|
||||||
|
is_auto_increment => 1,
|
||||||
|
},
|
||||||
|
username => {
|
||||||
|
data_type => 'TEXT',
|
||||||
|
is_nullable => 0,
|
||||||
|
},
|
||||||
|
password => {
|
||||||
|
data_type => 'TEXT',
|
||||||
|
is_nullable => 0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
__PACKAGE__->set_primary_key('id');
|
||||||
|
1;
|
62
lib/VPNManager/Schema/Result/VPNUser.pm
Normal file
62
lib/VPNManager/Schema/Result/VPNUser.pm
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
package VPNManager::Schema::Result::VPNUser;
|
||||||
|
|
||||||
|
use v5.38.2;
|
||||||
|
|
||||||
|
use strict;
|
||||||
|
use warnings;
|
||||||
|
|
||||||
|
use feature 'signatures';
|
||||||
|
|
||||||
|
use parent 'DBIx::Class::Core';
|
||||||
|
|
||||||
|
__PACKAGE__->table('vpn_users');
|
||||||
|
|
||||||
|
__PACKAGE__->add_columns(
|
||||||
|
id => {
|
||||||
|
data_type => 'INTEGER',
|
||||||
|
is_auto_increment => 1,
|
||||||
|
},
|
||||||
|
name => {
|
||||||
|
data_type => 'TEXT',
|
||||||
|
is_nullable => 0,
|
||||||
|
},
|
||||||
|
publickey => {
|
||||||
|
data_type => 'TEXT',
|
||||||
|
is_nullable => 0,
|
||||||
|
},
|
||||||
|
is_enabled => {
|
||||||
|
data_type => 'INTEGER',
|
||||||
|
is_nullable => 1,
|
||||||
|
},
|
||||||
|
is_protected => {
|
||||||
|
data_type => 'INTEGER',
|
||||||
|
is_nullable => 1,
|
||||||
|
},
|
||||||
|
is_deleted => {
|
||||||
|
data_type => 'INTEGER',
|
||||||
|
is_nullable => 1,
|
||||||
|
},
|
||||||
|
ip => {
|
||||||
|
data_type => 'INTEGER',
|
||||||
|
is_nullable => 0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
sub ip_to_text($self) {
|
||||||
|
my @octets;
|
||||||
|
for my $i (0..3) {
|
||||||
|
push @octets, ($self->ip >> (abs(3-$i) * 8)) & 0xff;
|
||||||
|
}
|
||||||
|
return join '.', @octets;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub ip_from_text($self, $ip) {
|
||||||
|
my @octets = split /\./, $ip;
|
||||||
|
my $raw_ip = 0;
|
||||||
|
for my $i (0..3) {
|
||||||
|
$raw_ip |= (($octets[$i] & 0xff) << (abs(3-$i) * 8));
|
||||||
|
}
|
||||||
|
$self->ip($raw_ip);
|
||||||
|
}
|
||||||
|
__PACKAGE__->set_primary_key('id');
|
||||||
|
1;
|
31
public/style.css
Normal file
31
public/style.css
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
body.login-body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.create-user {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.main {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
min-height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
form.inline {
|
||||||
|
display: inline;
|
||||||
|
}
|
29
script/create_user.pl
Normal file
29
script/create_user.pl
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
#!/usr/bin/env perl
|
||||||
|
|
||||||
|
use v5.38.2;
|
||||||
|
|
||||||
|
use strict;
|
||||||
|
use warnings;
|
||||||
|
|
||||||
|
use File::Basename qw/dirname/;
|
||||||
|
|
||||||
|
use lib dirname(dirname(__FILE__)).'/lib';
|
||||||
|
|
||||||
|
use Crypt::Bcrypt qw/bcrypt/;
|
||||||
|
use Crypt::URandom qw/urandom/;
|
||||||
|
use VPNManager::Schema;
|
||||||
|
|
||||||
|
my $username = $ARGV[0] or die 'No username passed';
|
||||||
|
my $password = $ARGV[1] or die 'No password passed';
|
||||||
|
|
||||||
|
my $new_salt = urandom(16);
|
||||||
|
my $encrypted_password = bcrypt $password, '2b', 12, $new_salt;
|
||||||
|
|
||||||
|
VPNManager::Schema->Schema->resultset('Admin')->populate(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
username => $username,
|
||||||
|
password => $encrypted_password,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
);
|
39
script/get_wg_config.pl
Normal file
39
script/get_wg_config.pl
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
#!/usr/bin/env perl
|
||||||
|
use v5.38.2;
|
||||||
|
|
||||||
|
use strict;
|
||||||
|
use warnings;
|
||||||
|
|
||||||
|
use Moo;
|
||||||
|
use File::Basename qw/dirname/;
|
||||||
|
use lib dirname(dirname(__FILE__)).'/lib';
|
||||||
|
use VPNManager::Schema;
|
||||||
|
|
||||||
|
sub get_vpn_settings($self) {
|
||||||
|
require VPNManager;
|
||||||
|
my $config = VPNManager->new->config;
|
||||||
|
my $vpn_config = <<"EOF";
|
||||||
|
[Interface]
|
||||||
|
Address = @{[$config->{vpn}{host}]}/@{[$config->{vpn}{submask}]}
|
||||||
|
MTU = @{[$config->{vpn}{mtu}]}
|
||||||
|
SaveConfig = false
|
||||||
|
ListenPort = @{[$config->{vpnclients}{server_port}]}
|
||||||
|
PrivateKey = @{[$config->{vpn}{privkey}]}
|
||||||
|
EOF
|
||||||
|
my $resultset = VPNManager::Schema->Schema->resultset('VPNUser');
|
||||||
|
my @users = $resultset->search( {} );
|
||||||
|
|
||||||
|
for my $user (@users) {
|
||||||
|
next if !$user->is_enabled;
|
||||||
|
|
||||||
|
$vpn_config .= <<"EOF";
|
||||||
|
|
||||||
|
[Peer]
|
||||||
|
PublicKey = @{[$user->publickey]}
|
||||||
|
AllowedIPs = @{[$user->ip_to_text]}/32
|
||||||
|
Endpoint = @{[$config->{vpn}{endpoint}]}:@{[$config->{vpnclients}{server_port}]}
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
say $vpn_config;
|
||||||
|
}
|
||||||
|
__PACKAGE__->new->get_vpn_settings;
|
42
script/installer.pl
Normal file
42
script/installer.pl
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
#!/usr/bin/env perl
|
||||||
|
|
||||||
|
use v5.38.2;
|
||||||
|
|
||||||
|
use strict;
|
||||||
|
use warnings;
|
||||||
|
use File::Basename qw/dirname/;
|
||||||
|
use Cwd 'abs_path';
|
||||||
|
|
||||||
|
if ($> != 0) {
|
||||||
|
die 'You must be root.';
|
||||||
|
}
|
||||||
|
|
||||||
|
while (1) {
|
||||||
|
eval {
|
||||||
|
install_if_new();
|
||||||
|
sleep 15;
|
||||||
|
};
|
||||||
|
if ($@) {
|
||||||
|
warn $@;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sub install_if_new {
|
||||||
|
my $script_get_wg_config = abs_path(dirname(__FILE__).'/get_wg_config.pl');
|
||||||
|
my $user = 'vpnmanager';
|
||||||
|
open my $fh, '-|', 'sudo', '-i', '-u', $user, 'perl', $script_get_wg_config;
|
||||||
|
my $contents = join '', <$fh>;
|
||||||
|
my $output_file = '/etc/wireguard/wg0.conf';
|
||||||
|
my $output_exists;
|
||||||
|
open $fh, '<', $output_file and ($output_exists = 1);
|
||||||
|
my $contents_output_file = '';
|
||||||
|
$contents_output_file = join '', <$fh> if $output_exists;
|
||||||
|
if ($contents ne $contents_output_file) {
|
||||||
|
say 'Writting new file';
|
||||||
|
open $fh, '>', $output_file;
|
||||||
|
print $fh $contents;
|
||||||
|
system 'systemctl', 'restart', 'vpnmanager';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
say 'Files equal';
|
||||||
|
}
|
11
script/vpnmanager
Executable file
11
script/vpnmanager
Executable file
@ -0,0 +1,11 @@
|
|||||||
|
#!/usr/bin/env perl
|
||||||
|
|
||||||
|
use strict;
|
||||||
|
use warnings;
|
||||||
|
|
||||||
|
use Mojo::File qw(curfile);
|
||||||
|
use lib curfile->dirname->sibling('lib')->to_string;
|
||||||
|
use Mojolicious::Commands;
|
||||||
|
|
||||||
|
# Start command line interface for application
|
||||||
|
Mojolicious::Commands->start_app('VPNManager');
|
5
templates/layouts/default.html.ep
Normal file
5
templates/layouts/default.html.ep
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head><title><%= title %></title></head>
|
||||||
|
<body><%= content %></body>
|
||||||
|
</html>
|
22
templates/main/index.html.ep
Normal file
22
templates/main/index.html.ep
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
% my $users = stash 'users';
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<link rel="stylesheet" href="/style.css"/>
|
||||||
|
</head>
|
||||||
|
<body class="main">
|
||||||
|
<p><a href="/vpn/create-user">Create new vpn user</a>.</p>
|
||||||
|
% if (defined $users && @$users) {
|
||||||
|
<ul>
|
||||||
|
% for my $user (@$users) {
|
||||||
|
<li>Id: <b><%=$user->id%></b> Name: <b><%=$user->name%></b> Internal IP Address: <b><%=$user->ip_to_text%></b> <a href="/vpn/user/<%=$user->id%>/details">View details</a>
|
||||||
|
<form class="inline" method="post" action="/vpn/user/<%=$user->id%>/<%=$user->is_enabled ? 'disable' : 'enable'%>">
|
||||||
|
<input type="submit" value="<%=$user->is_enabled? 'Disable' : 'Enable'%>"/>
|
||||||
|
</form></li>
|
||||||
|
% }
|
||||||
|
% }
|
||||||
|
</ul>
|
||||||
|
<form action="/vpn/save" method="post">
|
||||||
|
<input type="submit" value="Save VPN Settings"/>
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
20
templates/main/login.html.ep
Normal file
20
templates/main/login.html.ep
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<link rel="stylesheet" href="/style.css"/>
|
||||||
|
</head>
|
||||||
|
<body class="login-body">
|
||||||
|
<form class="login-form" action="/login" method="post">
|
||||||
|
<div class="login-field">
|
||||||
|
<label for="username">Username</label>
|
||||||
|
<input name="username" type="text"/>
|
||||||
|
</div>
|
||||||
|
<div class="login-field">
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<input name="password" type="password"/>
|
||||||
|
</div>
|
||||||
|
<div class="login-field">
|
||||||
|
<input type="submit" value="Submit"/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
14
templates/vpn/create-user.html.ep
Normal file
14
templates/vpn/create-user.html.ep
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<link rel="stylesheet" href="/style.css"/>
|
||||||
|
</head>
|
||||||
|
<body class="create-user">
|
||||||
|
<form action="/vpn/create-user" method="post">
|
||||||
|
<div>
|
||||||
|
<label for="name">Name</label>
|
||||||
|
<input name="name"/>
|
||||||
|
</div>
|
||||||
|
<input type="submit" value="Submit"/>
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
10
templates/vpn/download-file.html.ep
Normal file
10
templates/vpn/download-file.html.ep
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
% use MIME::Base64 qw/encode_base64/;
|
||||||
|
% my $file = stash 'vpn_file';
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<link rel="stylesheet" href="/style.css"/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<a href="/vpn/user/<%=$user->id%>/download" download="wg0.conf">Dowload vpn file</a>
|
||||||
|
</body>
|
||||||
|
</html>
|
16
templates/vpn/user-details.html.ep
Normal file
16
templates/vpn/user-details.html.ep
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
% use MIME::Base64 qw/encode_base64/;
|
||||||
|
% my $user = stash 'details_user';
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<link rel="stylesheet" href="/style.css"/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>VPN Account</h1>
|
||||||
|
<h2>Name: <%=$user->name%></h2>
|
||||||
|
<h2>Internal IP Address: <%=$user->ip_to_text%></h2>
|
||||||
|
<form action="/vpn/user/<%=$user->id%>/download" target="_blank" method="post">
|
||||||
|
<input type="submit" value="Download new VPN file"/>
|
||||||
|
</form>
|
||||||
|
<p><a href="/">Volver al menu principal</a></p>
|
||||||
|
</body>
|
||||||
|
</html>
|
16
v_p_n_manager.example.yml
Normal file
16
v_p_n_manager.example.yml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
secrets:
|
||||||
|
- generated_secret
|
||||||
|
vpn:
|
||||||
|
host: '192.168.2.1'
|
||||||
|
submask: 24
|
||||||
|
privkey: 'your_private_key'
|
||||||
|
listenport: 51820
|
||||||
|
mtu: 1420
|
||||||
|
endpoint: 'your_endpoint'
|
||||||
|
vpnclients:
|
||||||
|
submask: 32
|
||||||
|
allowedips: '192.168.2.0/24'
|
||||||
|
endpoint: 'your_endpoint'
|
||||||
|
server_port: 51820
|
||||||
|
starting_ip: '192.168.2.2'
|
Loading…
Reference in New Issue
Block a user