| 14 |
rodolico |
1 |
#! /usr/bin/env perl
|
|
|
2 |
|
|
|
3 |
|
|
|
4 |
use strict;
|
|
|
5 |
use warnings;
|
|
|
6 |
|
|
|
7 |
use lib '.'; # our libraries are in the current directory
|
|
|
8 |
|
|
|
9 |
use JSON; # for encode_json, decode_json
|
|
|
10 |
use Data::Dumper; # for debugging
|
|
|
11 |
use opnsense; # our module to handle opnsense API calls
|
|
|
12 |
use GD::Barcode::QRcode; # for creating the QR code images
|
|
|
13 |
use MIME::Base64 qw( decode_base64 ); # for decoding the ovpn file returned from the API
|
|
|
14 |
use File::Path qw( make_path ); # for creating directories
|
|
|
15 |
|
|
|
16 |
# Configuration file path
|
|
|
17 |
my $config_file = 'config.json';
|
|
|
18 |
# Users file path. This is shared between loadOpnSense.pl and index.php
|
|
|
19 |
my $users_file = 'users.json';
|
|
|
20 |
# Constants for creation of QR image
|
|
|
21 |
# this is used in URL created
|
|
|
22 |
my $issuer = "OPNsense";
|
|
|
23 |
# The size of individual "pixels" in graphics
|
|
|
24 |
my $moduleSize = 10;
|
|
|
25 |
# location to place the QR images
|
|
|
26 |
my $qrLocation = './qrcodes'; # cwd/qrcodes
|
|
|
27 |
# location to place the ovpn files
|
|
|
28 |
my $ovpnFileLocation = './openvpn_configs'; # cwd/openvpn
|
|
|
29 |
# following strings will be added to ovpn files if they don't exist
|
|
|
30 |
my $additionalOvpnStrings = "static-challenge 'Enter Auth Code' 0\nauth-nocache\n";
|
|
|
31 |
|
|
|
32 |
# load the configuration for the specified router from the config file
|
|
|
33 |
# returns a hash ref of the configuration for the specified entry (router name)
|
|
|
34 |
# if file does not exist or entry not found, returns empty hash ref
|
|
|
35 |
# file - name of file to read (default config.json if not specified)
|
|
|
36 |
# entry - name of the router to load configuration for
|
|
|
37 |
sub loadConfig {
|
|
|
38 |
my ($file, $entry) = @_;
|
|
|
39 |
if ( -e $file ) {
|
|
|
40 |
open my $fh, '<', $file or die "Could not open '$file' $!";
|
|
|
41 |
local $/;
|
|
|
42 |
my $json_text = <$fh>;
|
|
|
43 |
close $fh;
|
|
|
44 |
my $config = decode_json($json_text);
|
|
|
45 |
return $config->{$entry} if (exists $config->{$entry});
|
|
|
46 |
}
|
|
|
47 |
return {};
|
|
|
48 |
}
|
|
|
49 |
|
|
|
50 |
# load the users data structure from the specified file
|
|
|
51 |
sub loadUsers {
|
|
|
52 |
my ( $file ) = @_;
|
|
|
53 |
my $data = {};
|
|
|
54 |
if ( -e $file ) {
|
|
|
55 |
open( my $fh, '<', $file ) or die "Cannot open $file: $!";
|
|
|
56 |
local $/; # slurp mode
|
|
|
57 |
my $json_text = <$fh>;
|
|
|
58 |
close $fh;
|
|
|
59 |
$data = decode_json($json_text);
|
|
|
60 |
}
|
|
|
61 |
return $data;
|
|
|
62 |
}
|
|
|
63 |
|
|
|
64 |
# save the users data structure to the specified file
|
|
|
65 |
sub saveUsers {
|
|
|
66 |
my ( $file, $data ) = @_;
|
|
|
67 |
open( my $fh, '>', $file ) or die "Cannot open $file: $!";
|
|
|
68 |
print $fh encode_json($data);
|
|
|
69 |
close $fh;
|
|
|
70 |
}
|
|
|
71 |
|
|
|
72 |
#
|
|
|
73 |
# makeQR
|
|
|
74 |
#
|
|
|
75 |
# Creates a QR file with a name of ($routername_)$account.png
|
|
|
76 |
# where $routername is prepended if it is set and $account is the user name
|
|
|
77 |
# an underscore separates the routername and the account (if routername is set)
|
|
|
78 |
# the contents of the qr are made up of the constants 'otpauth://totp/' followed by $issuer
|
|
|
79 |
# and the parameters secret (otp code) and issuer again.
|
|
|
80 |
sub makeQR {
|
|
|
81 |
my ( $routerName, $account, $secret, $issuer, $moduleSize, $qrLocation ) = @_;
|
|
|
82 |
my $otp_url = "otpauth://totp/$issuer:$account?secret=$secret&issuer=$issuer";
|
|
|
83 |
# Ecc (Error Correction Code) can be one of Low, Medium, Quartile, and High
|
|
|
84 |
# ModuleSize increases the actual size of each "pixel", enlarging for easier reading
|
|
|
85 |
my $barcode = GD::Barcode::QRcode->new($otp_url, { Ecc => 'M', ModuleSize => $moduleSize } );
|
|
|
86 |
my $image = $barcode->plot();
|
|
|
87 |
# Save the image to a file. If there is a router name, prepend it to the filename with an underscore
|
|
|
88 |
# allows the same user to be on two different routers
|
|
|
89 |
my $outputFile = $qrLocation . '/' . ($routerName ? $routerName . '_' : '' ) . $account . '.png';
|
|
|
90 |
open my $out, '>', "$outputFile" or die "Cannot write file $outputFile: $!";
|
|
|
91 |
binmode $out;
|
|
|
92 |
print $out $image->png;
|
|
|
93 |
close $out;
|
|
|
94 |
return $outputFile;
|
|
|
95 |
}
|
|
|
96 |
|
|
|
97 |
#
|
|
|
98 |
# makeVPNConfigFile
|
|
|
99 |
#
|
|
|
100 |
# Creates a configuration file for the user based on the template provided
|
|
|
101 |
# The template is read in and the following substitutions are made:
|
|
|
102 |
sub makeVPNConfigFile {
|
|
|
103 |
my ( $contents, $routername, $username, $ovpnLocation, $additionalOvpnStrings ) = @_;
|
|
|
104 |
# if contents is empty or does not have a content key, return empty string
|
|
|
105 |
return '' unless $contents && $contents->{'content'};
|
|
|
106 |
my $outputFileName = $ovpnLocation . '/' . ($routername ? $routername . '_' : '' ) . $username . '.ovpn';
|
|
|
107 |
$contents->{'content'} = decode_base64($contents->{'content'});
|
|
|
108 |
$contents->{'content'} .= "\n";
|
|
|
109 |
$contents->{'content'} =~ s/(\r?\n)+/\n/g; # normalize line endings
|
|
|
110 |
# add any additional strings if they are not already present
|
|
|
111 |
my $comment = '';
|
|
|
112 |
if ( $contents->{'content'} !~ /# Added by loadOpnSense.pl/ ) {
|
|
|
113 |
$comment = "# Added by loadOpnSense.pl\n";
|
|
|
114 |
}
|
|
|
115 |
foreach my $string ( split( /\n/, $additionalOvpnStrings ) ) {
|
|
|
116 |
unless ( $contents->{'content'} =~ /\$string/ ) {
|
|
|
117 |
$contents->{'content'} .= "$comment$string\n";
|
|
|
118 |
$comment = ''; # only add comment once
|
|
|
119 |
}
|
|
|
120 |
}
|
|
|
121 |
open my $out, '>', "$outputFileName" or die "Cannot write file $outputFileName: $!";
|
|
|
122 |
print $out $contents->{'content'};
|
|
|
123 |
close $out;
|
|
|
124 |
return $outputFileName;
|
|
|
125 |
}
|
|
|
126 |
|
|
|
127 |
|
|
|
128 |
### Main program
|
|
|
129 |
|
|
|
130 |
# get the router name from the command line
|
|
|
131 |
my $router = shift @ARGV or die "Usage: $0 <router_name>\n";
|
|
|
132 |
# Load existing configuration or initialize a new one
|
|
|
133 |
my $config = &loadConfig($config_file, $router );
|
|
|
134 |
die "Configuration for router $router not found in $config_file\n" unless $config && ref($config) eq 'HASH';
|
|
|
135 |
|
|
|
136 |
# load the users file. We will update the entry for this router
|
|
|
137 |
my $users = &loadUsers( $users_file );
|
|
|
138 |
|
|
|
139 |
# if there is no entry for this router, create an empty one
|
|
|
140 |
$users->{$router} = {} unless exists $users->{$router};
|
|
|
141 |
$users->{$router}->{'qrLocation'} = $qrLocation unless exists $users->{$router}->{'qrLocation'};
|
|
|
142 |
$users->{$router}->{'ovpnLocation'} = $ovpnFileLocation unless exists $users->{$router}->{'ovpnLocation'};
|
|
|
143 |
$users->{$router}->{'users'} = {}; # clean out the users list, we will reload it
|
|
|
144 |
|
|
|
145 |
# ensure the directories exist and give them full access
|
|
|
146 |
make_path( $users->{$router}->{'qrLocation'}, 0777 ) unless -d $users->{$router}->{'qrLocation'};
|
|
|
147 |
make_path( $users->{$router}->{'ovpnLocation'}, 0777 ) unless -d $users->{$router}->{'ovpnLocation'};
|
|
|
148 |
|
|
|
149 |
# this does most of the work, all the API calls are handled in the module
|
|
|
150 |
# create the opnsense object
|
|
|
151 |
my $opnsense = new opnsense(
|
|
|
152 |
url => $config->{'url'},
|
|
|
153 |
apiKey => $config->{'apiKey'},
|
|
|
154 |
apiSecret => $config->{'apiSecret'},
|
|
|
155 |
ovpnIndex => $config->{'ovpnIndex'},
|
|
|
156 |
localport => $config->{'localport'},
|
|
|
157 |
template => $config->{'template'},
|
|
|
158 |
hostname => $config->{'hostname'},
|
|
|
159 |
);
|
|
|
160 |
|
|
|
161 |
# get the VPN users. This is a hashref keyed by cert name, value is username
|
|
|
162 |
my $vpnCerts = $opnsense->get_vpn_users();
|
|
|
163 |
|
|
|
164 |
# convert the cert-"user" to username->certs, array ref of certs as not sure if multiple certs per user allowed
|
|
|
165 |
foreach my $cert ( keys %$vpnCerts ) {
|
|
|
166 |
push @{$users->{$router}->{'users'}->{$vpnCerts->{$cert}}->{'certs'}}, $cert;
|
|
|
167 |
}
|
|
|
168 |
|
|
|
169 |
# these are all of the users on the system
|
|
|
170 |
my $allUsers = $opnsense->get_all_users();
|
|
|
171 |
|
|
|
172 |
# for each user in the system, if they are in the vpnUsers list, copy otp_seed, cert, and password
|
|
|
173 |
# we'll also delete any users who are disabled
|
|
|
174 |
my @keys = ('otp_seed', 'cert', 'password' ); # these are the keys we want to copy
|
|
|
175 |
foreach my $user ( keys %$allUsers ) {
|
|
|
176 |
next unless exists $users->{$router}->{'users'}->{$user}; # skip users who do not have vpn certs
|
|
|
177 |
if ( $allUsers->{$user}->{'disabled'} ) { # skip users who are disabled
|
|
|
178 |
delete $users->{$router}->{'users'}->{$user};
|
|
|
179 |
next;
|
|
|
180 |
}
|
|
|
181 |
foreach my $key ( @keys ) {
|
|
|
182 |
$users->{$router}->{'users'}->{$user}->{$key} = ref($allUsers->{$user}->{$key}) ? '' : $allUsers->{$user}->{$key};
|
|
|
183 |
}
|
|
|
184 |
}
|
|
|
185 |
|
|
|
186 |
# create the QR code file and VPN configuration for each user
|
|
|
187 |
foreach my $entry ( keys %{$users->{$router}->{'users'}} ) {
|
|
|
188 |
my $account = $entry;
|
|
|
189 |
my $secret = $users->{$router}->{'users'}->{$entry}->{'otp_seed'};
|
|
|
190 |
$users->{$router}->{'users'}->{$entry}->{'qrFile'} =
|
|
|
191 |
&makeQR($router, $account, $secret, $issuer, $moduleSize, $users->{$router}->{'qrLocation'});
|
|
|
192 |
if ( scalar(@{$users->{$router}->{'users'}->{$entry}->{'certs'}}) > 1 ) {
|
|
|
193 |
warn "$entry has multiple certs, using first one only\n";
|
|
|
194 |
}
|
|
|
195 |
my $cert = $opnsense->get_vpn_config( $users->{$router}->{'users'}->{$entry}->{'certs'}->[0] );
|
|
|
196 |
if ( !$cert || ref($cert) ne 'HASH' || !exists $cert->{'content'} ) {
|
|
|
197 |
warn "Could not get VPN configuration for $entry, cert $users->{$router}->{'users'}->{$entry}->{'certs'}->[0]\n";
|
|
|
198 |
next;
|
|
|
199 |
}
|
|
|
200 |
$users->{$router}->{'users'}->{$entry}->{'ovpnFile'} =
|
|
|
201 |
&makeVPNConfigFile( $cert, $router, $entry, $users->{$router}->{'ovpnLocation'}, $additionalOvpnStrings );
|
|
|
202 |
}
|
|
|
203 |
|
|
|
204 |
# save the users file
|
|
|
205 |
&saveUsers( $users_file, $users );
|