| Line 1... |
Line 1... |
| 1 |
#! /usr/bin/env perl
|
1 |
#! /usr/bin/env perl
|
| - |
|
2 |
|
| - |
|
3 |
# Copyright (c) 2025, Daily Data, Inc.
|
| - |
|
4 |
# All rights reserved.
|
| - |
|
5 |
#
|
| - |
|
6 |
# Redistribution and use in source and binary forms, with or without modification,
|
| - |
|
7 |
# are permitted provided that the following conditions are met:
|
| 2 |
|
8 |
#
|
| - |
|
9 |
# 1. Redistributions of source code must retain the above copyright notice, this list
|
| - |
|
10 |
# of conditions and the following disclaimer.
|
| - |
|
11 |
# 2. Redistributions in binary form must reproduce the above copyright notice, this
|
| - |
|
12 |
# list of conditions and the following disclaimer in the documentation and/or other
|
| - |
|
13 |
# materials provided with the distribution.
|
| - |
|
14 |
# 3. Neither the name of Daily Data, Inc. nor the names of its contributors may be
|
| - |
|
15 |
# used to endorse or promote products derived from this software without specific
|
| - |
|
16 |
# prior written permission.
|
| - |
|
17 |
#
|
| - |
|
18 |
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
|
| - |
|
19 |
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
| - |
|
20 |
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
|
| - |
|
21 |
# SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
| - |
|
22 |
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
| - |
|
23 |
# TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
|
| - |
|
24 |
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
| - |
|
25 |
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
| - |
|
26 |
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
|
| - |
|
27 |
# DAMAGE.
|
| - |
|
28 |
#
|
| - |
|
29 |
# Script which will read information from an OPNsense router via its API
|
| - |
|
30 |
# and create/update user OTP secrets, QR codes, and OpenVPN configuration files
|
| - |
|
31 |
# for each user with a VPN certificate.
|
| 3 |
|
32 |
#
|
| - |
|
33 |
# Change History:
|
| - |
|
34 |
# v1.0 2025-10-01 - Initial version RWR
|
| - |
|
35 |
# Initial Release
|
| - |
|
36 |
# v1.0.1 2025-10-01 RWR
|
| - |
|
37 |
# Added a lock file to not allow the script to run if it is already running. If we can not
|
| - |
|
38 |
# get an exclusive lock on $lockFile in $lockRetryTime*$lockRetries seconds, script will abort
|
| - |
|
39 |
# Added both the full path to the data directories as well as the relative, to help
|
| - |
|
40 |
# the web site (relative) and the script (full path, even if we're running from a different)
|
| - |
|
41 |
# location to work easily (less code)
|
| - |
|
42 |
|
| 4 |
use strict;
|
43 |
use strict;
|
| 5 |
use warnings;
|
44 |
use warnings;
|
| 6 |
|
45 |
|
| - |
|
46 |
# use libraries from the directory this script is in
|
| - |
|
47 |
BEGIN {
|
| - |
|
48 |
use FindBin;
|
| - |
|
49 |
use File::Spec;
|
| 7 |
use lib '.'; # our libraries are in the current directory
|
50 |
# use libraries from the directory this script is in
|
| - |
|
51 |
use Cwd 'abs_path';
|
| - |
|
52 |
use File::Basename;
|
| - |
|
53 |
use lib dirname( abs_path( __FILE__ ) );
|
| - |
|
54 |
}
|
| - |
|
55 |
|
| 8 |
|
56 |
|
| 9 |
use JSON; # for encode_json, decode_json
|
57 |
use JSON; # for encode_json, decode_json
|
| 10 |
use Data::Dumper; # for debugging
|
58 |
use Data::Dumper; # for debugging
|
| 11 |
use opnsense; # our module to handle opnsense API calls
|
59 |
use opnsense; # our module to handle opnsense API calls
|
| 12 |
use GD::Barcode::QRcode; # for creating the QR code images
|
60 |
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
|
61 |
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
|
62 |
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
|
63 |
use Fcntl qw(:flock); # Import locking constants
|
| 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 |
|
64 |
|
| 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 {
|
65 |
our $VERSION = '1.0.1';
|
| - |
|
66 |
|
| 38 |
my ($file, $entry) = @_;
|
67 |
# Check if running as root
|
| 39 |
if ( -e $file ) {
|
68 |
if ($> != 0) {
|
| 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);
|
69 |
die "Error: This script must be run as root.\n";
|
| 45 |
return $config->{$entry} if (exists $config->{$entry});
|
- |
|
| 46 |
}
|
- |
|
| 47 |
return {};
|
- |
|
| 48 |
}
|
70 |
}
|
| 49 |
|
71 |
|
| - |
|
72 |
#-----------------------------
|
| - |
|
73 |
# Configuration Variables
|
| - |
|
74 |
#-----------------------------
|
| - |
|
75 |
# Location of the configuration files and directories where we will store data
|
| - |
|
76 |
# relative to the script directory as written
|
| - |
|
77 |
my $scriptDir = $FindBin::RealBin;
|
| - |
|
78 |
my $configFile = $scriptDir . '/routers.json';
|
| - |
|
79 |
my $usersFile = $scriptDir . '/users.json';
|
| - |
|
80 |
my $qrLocation = './qrcodes';
|
| - |
|
81 |
my $ovpnFileLocation = './openvpn_configs';
|
| - |
|
82 |
# size of the QR code modules, basically a magnification factor
|
| - |
|
83 |
my $moduleSize = 10;
|
| 50 |
# load the users data structure from the specified file
|
84 |
# strings to add to the ovpn file if not already present
|
| - |
|
85 |
my $additionalOvpnStrings = "static-challenge 'Enter Auth Code' 0\nauth-nocache\n";
|
| - |
|
86 |
# using a lock file to ensure script does not run while another request has it running
|
| - |
|
87 |
my $lockFile = '/tmp/opnsense.lock';
|
| - |
|
88 |
my $lockRetryTime = 10; # number of seconds between subsequent tries for a lock
|
| - |
|
89 |
my $lockRetries = 6; # number of times we will attempt to get a lock before failing
|
| - |
|
90 |
|
| - |
|
91 |
#-----------------------------
|
| - |
|
92 |
# slurpFile: Reads in the entire contents of a file and returns it as a string
|
| - |
|
93 |
# $file - name of file to read
|
| - |
|
94 |
# returns the contents of the file as a string, or empty string if file does not exist
|
| - |
|
95 |
# since we do this a lot, make it a separate function. This is likely the most efficient way
|
| - |
|
96 |
# to read a file in Perl
|
| - |
|
97 |
#-----------------------------
|
| 51 |
sub loadUsers {
|
98 |
sub slurpFile {
|
| 52 |
my ( $file ) = @_;
|
99 |
my ($file) = @_;
|
| 53 |
my $data = {};
|
100 |
my $data = '';
|
| 54 |
if ( -e $file ) {
|
101 |
if (-e $file) {
|
| 55 |
open( my $fh, '<', $file ) or die "Cannot open $file: $!";
|
102 |
open(my $fh, '<', $file) or die "Cannot open $file: $!";
|
| 56 |
local $/; # slurp mode
|
103 |
local $/; # slurp mode
|
| 57 |
my $json_text = <$fh>;
|
104 |
$data = <$fh>;
|
| 58 |
close $fh;
|
105 |
close $fh;
|
| 59 |
$data = decode_json($json_text);
|
- |
|
| 60 |
}
|
106 |
}
|
| 61 |
return $data;
|
107 |
return $data;
|
| 62 |
}
|
108 |
}
|
| 63 |
|
109 |
|
| - |
|
110 |
#-----------------------------
|
| - |
|
111 |
# loadConfig: Load router configuration from config file
|
| - |
|
112 |
# $file - name of file to read
|
| - |
|
113 |
# $entry - name of the router entry to load
|
| - |
|
114 |
# returns a reference to the data structure for the router, or empty hash if not found
|
| - |
|
115 |
#-----------------------------
|
| - |
|
116 |
sub loadConfig {
|
| - |
|
117 |
my ($file, $entry) = @_;
|
| - |
|
118 |
my $return = &slurpFile($file);
|
| - |
|
119 |
return {} unless $return && $return ne '';
|
| - |
|
120 |
my $data = decode_json($return);
|
| - |
|
121 |
return $data->{$entry} if (exists $data->{$entry});
|
| - |
|
122 |
return {};
|
| - |
|
123 |
}
|
| - |
|
124 |
|
| - |
|
125 |
#-----------------------------
|
| - |
|
126 |
# loadUsers: Load users data structure from file
|
| - |
|
127 |
# $file - name of file to read
|
| - |
|
128 |
# returns a reference to the data structure
|
| - |
|
129 |
# $file assumed to be in JSON format
|
| - |
|
130 |
#-----------------------------
|
| - |
|
131 |
sub loadUsers {
|
| - |
|
132 |
my ($file) = @_;
|
| - |
|
133 |
my $data = &slurpFile($file);
|
| - |
|
134 |
return {} unless $data && $data ne '';
|
| - |
|
135 |
my $return = decode_json($data);
|
| - |
|
136 |
return {} unless $return && ref($return) eq 'HASH';
|
| - |
|
137 |
return $return;
|
| - |
|
138 |
}
|
| - |
|
139 |
|
| - |
|
140 |
#-----------------------------
|
| 64 |
# save the users data structure to the specified file
|
141 |
# saveUsers: Save users data structure to file
|
| - |
|
142 |
# $file - name of file to write
|
| - |
|
143 |
# $data - reference to the data to write
|
| - |
|
144 |
# $timeStampFile - optional name of a file to write the current timestamp to
|
| - |
|
145 |
# writes the data in JSON format
|
| - |
|
146 |
#-----------------------------
|
| 65 |
sub saveUsers {
|
147 |
sub saveUsers {
|
| 66 |
my ( $file, $data ) = @_;
|
148 |
my ($file, $data) = @_;
|
| 67 |
open( my $fh, '>', $file ) or die "Cannot open $file: $!";
|
149 |
open(my $fh, '>', $file) or die "Cannot open $file: $!";
|
| 68 |
print $fh encode_json($data);
|
150 |
print $fh encode_json($data);
|
| 69 |
close $fh;
|
151 |
close $fh;
|
| 70 |
}
|
152 |
}
|
| 71 |
|
153 |
|
| 72 |
#
|
- |
|
| - |
|
154 |
#-----------------------------
|
| 73 |
# makeQR
|
155 |
# makeQR: Creates a QR code file
|
| 74 |
#
|
- |
|
| 75 |
# Creates a QR file with a name of ($routername_)$account.png
|
156 |
# $routerName - name of the router (used in filename and OTP URL)
|
| 76 |
# where $routername is prepended if it is set and $account is the user name
|
157 |
# $account - account name (used in filename and OTP URL)
|
| - |
|
158 |
# $secret - OTP secret
|
| - |
|
159 |
# $moduleSize - size of the QR code modules
|
| 77 |
# an underscore separates the routername and the account (if routername is set)
|
160 |
# $relativeDir - directory to save the QR code file
|
| 78 |
# the contents of the qr are made up of the constants 'otpauth://totp/' followed by $issuer
|
161 |
# $absDir - The directory the script is in, so writing relative directories will correctly place
|
| 79 |
# and the parameters secret (otp code) and issuer again.
|
162 |
# returns the filename of the created QR code image
|
| - |
|
163 |
#-----------------------------
|
| 80 |
sub makeQR {
|
164 |
sub makeQR {
|
| - |
|
165 |
# Creates a QR code image for the user's OTP secret
|
| 81 |
my ( $routerName, $account, $secret, $issuer, $moduleSize, $qrLocation ) = @_;
|
166 |
my ($routerName, $account, $secret, $moduleSize, $relativeDir, $absDir ) = @_;
|
| 82 |
my $otp_url = "otpauth://totp/$issuer:$account?secret=$secret&issuer=$issuer";
|
167 |
# build the OTP URL to include router name as issuer and the account name for display in authenticator apps
|
| 83 |
# Ecc (Error Correction Code) can be one of Low, Medium, Quartile, and High
|
168 |
my $otpUrl = "otpauth://totp/$routerName:$account?secret=$secret&issuer=$routerName";
|
| 84 |
# ModuleSize increases the actual size of each "pixel", enlarging for easier reading
|
169 |
# generate the QR code
|
| 85 |
my $barcode = GD::Barcode::QRcode->new($otp_url, { Ecc => 'M', ModuleSize => $moduleSize } );
|
170 |
my $barcode = GD::Barcode::QRcode->new($otpUrl, { Ecc => 'M', ModuleSize => $moduleSize } );
|
| 86 |
my $image = $barcode->plot();
|
171 |
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
|
172 |
my $fileName = ($routerName ? $routerName . '_' : '' ) . $account . '.png';
|
| 88 |
# allows the same user to be on two different routers
|
173 |
# This allows us to retain the relative file path for storage, but ensuring we write the file to
|
| 89 |
my $outputFile = $qrLocation . '/' . ($routerName ? $routerName . '_' : '' ) . $account . '.png';
|
174 |
# the correct path even if we are not running the script from the script directory.
|
| 90 |
open my $out, '>', "$outputFile" or die "Cannot write file $outputFile: $!";
|
175 |
open my $out, '>', "$absDir/" . $fileName or die "Cannot write file $fileName to $absDir: $!";
|
| 91 |
binmode $out;
|
176 |
binmode $out;
|
| 92 |
print $out $image->png;
|
177 |
print $out $image->png;
|
| 93 |
close $out;
|
178 |
close $out;
|
| 94 |
return $outputFile;
|
179 |
return $relativeDir . '/' . $fileName;
|
| 95 |
}
|
180 |
}
|
| 96 |
|
181 |
|
| 97 |
#
|
- |
|
| 98 |
# makeVPNConfigFile
|
182 |
#-----------------------------
|
| 99 |
#
|
- |
|
| 100 |
# Creates a configuration file for the user based on the template provided
|
183 |
# makeVPNConfigFile: Creates a VPN configuration file for the user
|
| - |
|
184 |
# $contents - reference to the hash containing the 'content' key with base64 encoded ovpn file
|
| 101 |
# The template is read in and the following substitutions are made:
|
185 |
# $routerName - name of the router (used in filename)
|
| - |
|
186 |
# $userName - name of the user (used in filename)
|
| - |
|
187 |
# $ovpnLocation - The relative path to the output directory
|
| - |
|
188 |
# $absDir - The absolute path to the output directory
|
| - |
|
189 |
# $additionalOvpnStrings - additional strings to add to the ovpn file if not already present
|
| - |
|
190 |
# returns the filename of the created ovpn file
|
| - |
|
191 |
#-----------------------------
|
| 102 |
sub makeVPNConfigFile {
|
192 |
sub makeVPNConfigFile {
|
| 103 |
my ( $contents, $routername, $username, $ovpnLocation, $additionalOvpnStrings ) = @_;
|
193 |
my ($contents, $routerName, $userName, $ovpnLocation, $absDir, $additionalOvpnStrings) = @_;
|
| 104 |
# if contents is empty or does not have a content key, return empty string
|
- |
|
| 105 |
return '' unless $contents && $contents->{'content'};
|
194 |
return '' unless $contents && $contents->{'content'};
|
| 106 |
my $outputFileName = $ovpnLocation . '/' . ($routername ? $routername . '_' : '' ) . $username . '.ovpn';
|
195 |
my $fileName = ($routerName ? $routerName . '_' : '' ) . $userName . '.ovpn';
|
| 107 |
$contents->{'content'} = decode_base64($contents->{'content'});
|
196 |
$contents->{'content'} = decode_base64($contents->{'content'});
|
| 108 |
$contents->{'content'} .= "\n";
|
197 |
$contents->{'content'} .= "\n";
|
| 109 |
$contents->{'content'} =~ s/(\r?\n)+/\n/g; # normalize line endings
|
198 |
$contents->{'content'} =~ s/(\r?\n)+/\n/g; # normalize line endings
|
| 110 |
# add any additional strings if they are not already present
|
- |
|
| 111 |
my $comment = '';
|
199 |
my $comment = '';
|
| 112 |
if ( $contents->{'content'} !~ /# Added by loadOpnSense.pl/ ) {
|
200 |
if ( $contents->{'content'} !~ /# Added by loadOpnSense.pl/ ) {
|
| 113 |
$comment = "# Added by loadOpnSense.pl\n";
|
201 |
$comment = "# Added by loadOpnSense.pl\n";
|
| 114 |
}
|
202 |
}
|
| 115 |
foreach my $string ( split( /\n/, $additionalOvpnStrings ) ) {
|
203 |
foreach my $string ( split( /\n/, $additionalOvpnStrings ) ) {
|
| 116 |
unless ( $contents->{'content'} =~ /\$string/ ) {
|
204 |
unless ( $contents->{'content'} =~ /\$string/ ) {
|
| 117 |
$contents->{'content'} .= "$comment$string\n";
|
205 |
$contents->{'content'} .= "$comment$string\n";
|
| 118 |
$comment = ''; # only add comment once
|
206 |
$comment = ''; # only add comment once
|
| 119 |
}
|
207 |
}
|
| 120 |
}
|
208 |
}
|
| - |
|
209 |
# This allows us to retain the relative file path for storage, but ensuring we write the file to
|
| - |
|
210 |
# the correct path even if we are not running the script from the script directory.
|
| 121 |
open my $out, '>', "$outputFileName" or die "Cannot write file $outputFileName: $!";
|
211 |
open my $out, '>', "$absDir/" . $fileName or die "Cannot write file $fileName to $absDir: $!";
|
| 122 |
print $out $contents->{'content'};
|
212 |
print $out $contents->{'content'};
|
| 123 |
close $out;
|
213 |
close $out;
|
| 124 |
return $outputFileName;
|
214 |
return $ovpnLocation . '/' . $fileName;
|
| - |
|
215 |
}
|
| - |
|
216 |
|
| - |
|
217 |
# cleanOldDataFiles: Deletes old data files in the specified directory matching the given pattern
|
| - |
|
218 |
# $directory - directory to scan for old files
|
| - |
|
219 |
# $pattern - regex pattern to match files to delete
|
| - |
|
220 |
# simply removes all files matching the pattern
|
| - |
|
221 |
#-----------------------------
|
| - |
|
222 |
sub cleanOldDataFiles {
|
| - |
|
223 |
my ( $directory, $pattern ) = @_;
|
| - |
|
224 |
opendir(my $dh, $directory) or die "Cannot open directory $directory: $!";
|
| - |
|
225 |
while (my $file = readdir($dh)) {
|
| - |
|
226 |
next if ($file =~ /^\./);
|
| - |
|
227 |
if ($file =~ /$pattern/) {
|
| - |
|
228 |
unlink "$directory/$file" or warn "Could not delete $directory/$file: $!";
|
| - |
|
229 |
}
|
| - |
|
230 |
}
|
| - |
|
231 |
closedir($dh);
|
| 125 |
}
|
232 |
}
|
| 126 |
|
233 |
|
| 127 |
|
234 |
|
| - |
|
235 |
#-----------------------------
|
| 128 |
### Main program
|
236 |
# Main program
|
| - |
|
237 |
#-----------------------------
|
| - |
|
238 |
|
| - |
|
239 |
# make sure script does not run more than once at the same time
|
| - |
|
240 |
# Open the lock file
|
| - |
|
241 |
open(my $fh, '>', $lockFile) or die "Cannot open lock file $lockFile: $!";
|
| - |
|
242 |
# Attempt to acquire an exclusive lock. We will try $lockTimeOut times, each
|
| - |
|
243 |
# waiting $lockRetryTime
|
| - |
|
244 |
while ( $lockRetries-- ) {
|
| - |
|
245 |
if (flock($fh, LOCK_EX | LOCK_NB)) {
|
| - |
|
246 |
last;
|
| - |
|
247 |
} else {
|
| - |
|
248 |
sleep($lockRetryTime);
|
| - |
|
249 |
}
|
| - |
|
250 |
}
|
| - |
|
251 |
# if $lockTimeOut is zero, we could not get a lock in, so die
|
| - |
|
252 |
die "Another instance of the script is already running and retries exceeded!\n" unless ( $lockRetries );
|
| 129 |
|
253 |
|
| 130 |
# get the router name from the command line
|
254 |
# get the router name from the command line
|
| 131 |
my $router = shift @ARGV or die "Usage: $0 <router_name>\n";
|
255 |
my $router = shift @ARGV or die "Usage: $0 <router_name>\n";
|
| 132 |
# Load existing configuration or initialize a new one
|
256 |
# Load existing configuration or initialize a new one
|
| 133 |
my $config = &loadConfig($config_file, $router );
|
257 |
my $config = &loadConfig($configFile, $router );
|
| 134 |
die "Configuration for router $router not found in $config_file\n" unless $config && ref($config) eq 'HASH';
|
258 |
die "Configuration for router $router not found in $configFile\n" unless $config && ref($config) eq 'HASH';
|
| 135 |
|
259 |
|
| 136 |
# load the users file. We will update the entry for this router
|
260 |
# load the users file. We will update the entry for this router
|
| 137 |
my $users = &loadUsers( $users_file );
|
261 |
my $users = &loadUsers( $usersFile );
|
| 138 |
|
262 |
|
| 139 |
# if there is no entry for this router, create an empty one
|
263 |
# if there is no entry for this router, create an empty one
|
| 140 |
$users->{$router} = {} unless exists $users->{$router};
|
264 |
$users->{$router} = {} unless exists $users->{$router};
|
| 141 |
$users->{$router}->{'qrLocation'} = $qrLocation unless exists $users->{$router}->{'qrLocation'};
|
265 |
$users->{$router}->{'qrLocation'} = $qrLocation unless exists $users->{$router}->{'qrLocation'};
|
| 142 |
$users->{$router}->{'ovpnLocation'} = $ovpnFileLocation unless exists $users->{$router}->{'ovpnLocation'};
|
266 |
$users->{$router}->{'ovpnLocation'} = $ovpnFileLocation unless exists $users->{$router}->{'ovpnLocation'};
|
| 143 |
$users->{$router}->{'users'} = {}; # clean out the users list, we will reload it
|
267 |
$users->{$router}->{'users'} = {}; # clean out the users list, we will reload it
|
| - |
|
268 |
# and get the location on the file system in case we are not running the script from the script directory
|
| - |
|
269 |
$users->{$router}->{'qrLocationFileystem'} = abs_path( $scriptDir . '/' . $qrLocation )
|
| - |
|
270 |
unless exists $users->{$router}->{'qrLocationFileystem'};
|
| - |
|
271 |
$users->{$router}->{'ovpnLocationFileSystem'} = abs_path( $scriptDir . '/' . $ovpnFileLocation )
|
| - |
|
272 |
unless exists $users->{$router}->{'ovpnLocationFileSystem'};
|
| - |
|
273 |
|
| 144 |
|
274 |
|
| 145 |
# ensure the directories exist and give them full access
|
275 |
# ensure the directories exist and give them full access
|
| 146 |
make_path( $users->{$router}->{'qrLocation'}, 0777 ) unless -d $users->{$router}->{'qrLocation'};
|
276 |
make_path( $users->{$router}->{'qrLocationFileystem'}, 0777 ) unless -d $users->{$router}->{'qrLocationFileystem'};
|
| 147 |
make_path( $users->{$router}->{'ovpnLocation'}, 0777 ) unless -d $users->{$router}->{'ovpnLocation'};
|
277 |
make_path( $users->{$router}->{'ovpnLocationFileSystem'}, 0777 ) unless -d $users->{$router}->{'ovpnLocationFileSystem'};
|
| - |
|
278 |
|
| - |
|
279 |
|
| - |
|
280 |
|
| - |
|
281 |
# instead of actually going through and cleaning files no longer in the users list,
|
| - |
|
282 |
# just delete all files for this router and recreate them
|
| - |
|
283 |
&cleanOldDataFiles( $users->{$router}->{'qrLocationFileystem'}, qr/^\Q$router\E_.*\.png$/ );
|
| - |
|
284 |
&cleanOldDataFiles( $users->{$router}->{'ovpnLocationFileSystem'}, qr/^\Q$router\E_.*\.ovpn$/ );
|
| 148 |
|
285 |
|
| 149 |
# this does most of the work, all the API calls are handled in the module
|
286 |
# this does most of the work, all the API calls are handled in the module
|
| 150 |
# create the opnsense object
|
287 |
# create the opnsense object
|
| 151 |
my $opnsense = new opnsense(
|
288 |
my $opnsense = opnsense->new(
|
| 152 |
url => $config->{'url'},
|
289 |
url => $config->{'url'},
|
| 153 |
apiKey => $config->{'apiKey'},
|
290 |
apiKey => $config->{'apiKey'},
|
| 154 |
apiSecret => $config->{'apiSecret'},
|
291 |
apiSecret => $config->{'apiSecret'},
|
| 155 |
ovpnIndex => $config->{'ovpnIndex'},
|
292 |
ovpnIndex => $config->{'ovpnIndex'},
|
| 156 |
localport => $config->{'localport'},
|
293 |
localPort => $config->{'localPort'},
|
| 157 |
template => $config->{'template'},
|
294 |
template => $config->{'template'},
|
| 158 |
hostname => $config->{'hostname'},
|
295 |
hostname => $config->{'hostname'},
|
| 159 |
);
|
296 |
);
|
| 160 |
|
297 |
|
| 161 |
# get the VPN users. This is a hashref keyed by cert name, value is username
|
298 |
# get the VPN users. This is a hashref keyed by cert name, value is username
|
| 162 |
my $vpnCerts = $opnsense->get_vpn_users();
|
299 |
my $vpnCerts = $opnsense->getVpnUsers();
|
| 163 |
|
300 |
|
| 164 |
# convert the cert-"user" to username->certs, array ref of certs as not sure if multiple certs per user allowed
|
301 |
# 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 ) {
|
302 |
foreach my $cert ( keys %$vpnCerts ) {
|
| 166 |
push @{$users->{$router}->{'users'}->{$vpnCerts->{$cert}}->{'certs'}}, $cert;
|
303 |
push @{$users->{$router}->{'users'}->{$vpnCerts->{$cert}}->{'certs'}}, $cert;
|
| 167 |
}
|
304 |
}
|
| 168 |
|
305 |
|
| 169 |
# these are all of the users on the system
|
306 |
# these are all of the users on the system
|
| 170 |
my $allUsers = $opnsense->get_all_users();
|
307 |
my $allUsers = $opnsense->getAllUsers();
|
| 171 |
|
308 |
|
| 172 |
# for each user in the system, if they are in the vpnUsers list, copy otp_seed, cert, and password
|
309 |
# 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
|
310 |
# we'll also delete any users who are disabled
|
| 174 |
my @keys = ('otp_seed', 'cert', 'password' ); # these are the keys we want to copy
|
311 |
my @keys = ('otp_seed', 'cert', 'password' ); # these are the keys we want to copy
|
| 175 |
foreach my $user ( keys %$allUsers ) {
|
312 |
foreach my $user ( keys %$allUsers ) {
|
| Line 184... |
Line 321... |
| 184 |
}
|
321 |
}
|
| 185 |
|
322 |
|
| 186 |
# create the QR code file and VPN configuration for each user
|
323 |
# create the QR code file and VPN configuration for each user
|
| 187 |
foreach my $entry ( keys %{$users->{$router}->{'users'}} ) {
|
324 |
foreach my $entry ( keys %{$users->{$router}->{'users'}} ) {
|
| 188 |
my $account = $entry;
|
325 |
my $account = $entry;
|
| 189 |
my $secret = $users->{$router}->{'users'}->{$entry}->{'otp_seed'};
|
326 |
my $secret = $users->{$router}->{'users'}->{$entry}->{'otp_seed'} || '';
|
| - |
|
327 |
# Create the QR code file and store the filename in the data structure
|
| - |
|
328 |
# If there is no secret, do not create a QR code
|
| 190 |
$users->{$router}->{'users'}->{$entry}->{'qrFile'} =
|
329 |
$users->{$router}->{'users'}->{$entry}->{'qrFile'} =
|
| - |
|
330 |
$secret && $secret ne '' ?
|
| - |
|
331 |
&makeQR(
|
| - |
|
332 |
$router,
|
| - |
|
333 |
$account,
|
| - |
|
334 |
$secret,
|
| - |
|
335 |
$moduleSize,
|
| 191 |
&makeQR($router, $account, $secret, $issuer, $moduleSize, $users->{$router}->{'qrLocation'});
|
336 |
$users->{$router}->{'qrLocation'},
|
| - |
|
337 |
$users->{$router}->{'qrLocationFileystem'}
|
| - |
|
338 |
) :
|
| - |
|
339 |
'';
|
| - |
|
340 |
|
| - |
|
341 |
# Create the VPN configuration file, if it exists. If multiple certs, only create the first one
|
| - |
|
342 |
# warn user if there are multiple certs
|
| 192 |
if ( scalar(@{$users->{$router}->{'users'}->{$entry}->{'certs'}}) > 1 ) {
|
343 |
if ( scalar(@{$users->{$router}->{'users'}->{$entry}->{'certs'}}) > 1 ) {
|
| 193 |
warn "$entry has multiple certs, using first one only\n";
|
344 |
warn "$entry has multiple certs, using first one only\n";
|
| 194 |
}
|
345 |
}
|
| 195 |
my $cert = $opnsense->get_vpn_config( $users->{$router}->{'users'}->{$entry}->{'certs'}->[0] );
|
346 |
my $cert = $opnsense->getVpnConfig( $users->{$router}->{'users'}->{$entry}->{'certs'}->[0] );
|
| 196 |
if ( !$cert || ref($cert) ne 'HASH' || !exists $cert->{'content'} ) {
|
347 |
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";
|
348 |
warn "Could not get VPN configuration for $entry, cert $users->{$router}->{'users'}->{$entry}->{'certs'}->[0]\n";
|
| 198 |
next;
|
349 |
next;
|
| 199 |
}
|
350 |
}
|
| 200 |
$users->{$router}->{'users'}->{$entry}->{'ovpnFile'} =
|
351 |
$users->{$router}->{'users'}->{$entry}->{'ovpnFile'} =
|
| - |
|
352 |
&makeVPNConfigFile(
|
| - |
|
353 |
$cert,
|
| - |
|
354 |
$router,
|
| - |
|
355 |
$entry,
|
| - |
|
356 |
$users->{$router}->{'ovpnLocation'},
|
| 201 |
&makeVPNConfigFile( $cert, $router, $entry, $users->{$router}->{'ovpnLocation'}, $additionalOvpnStrings );
|
357 |
$users->{$router}->{'ovpnLocationFileSystem'},
|
| - |
|
358 |
$additionalOvpnStrings );
|
| 202 |
}
|
359 |
}
|
| 203 |
|
- |
|
| - |
|
360 |
# update the timestamp for the current router
|
| - |
|
361 |
$users->{$router}->{'lastUpdate'} = time;
|
| 204 |
# save the users file
|
362 |
# save the users file
|
| 205 |
&saveUsers( $users_file, $users );
|
363 |
&saveUsers( $usersFile, $users );
|
| - |
|
364 |
|
| - |
|
365 |
# Remove the lock file so the script can run again
|
| - |
|
366 |
close($fh);
|
| - |
|
367 |
unlink($lockFile) or warn "Could not unlink lock file: $!";
|
| - |
|
368 |
|
| - |
|
369 |
1;
|