| 14 |
rodolico |
1 |
#! /usr/bin/env perl
|
| 16 |
rodolico |
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:
|
|
|
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.
|
|
|
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)
|
| 20 |
rodolico |
42 |
# v1.1.0 2026-01-04 RWR
|
|
|
43 |
# Removed hardcoded additionalOvpnStrings variable
|
|
|
44 |
# Added support for formats hash in router configuration
|
|
|
45 |
# Modified makeVPNConfigFile to accept filename template with ROUTER and USER placeholders
|
|
|
46 |
# Added support for multiple OVPN files per user based on formats configuration
|
|
|
47 |
# Formats structure allows specifying filename and additionalStrings per format
|
|
|
48 |
# Expands \n in additionalStrings to actual newlines in config files
|
| 21 |
rodolico |
49 |
# v1.1.1 2026-06-15 RWR
|
|
|
50 |
# Decodes base64 OVPN content only once per user, not per format
|
|
|
51 |
# Fixed bug in makeVPNConfigFile where content was being overwritten
|
|
|
52 |
# Cleans out old QR code and OVPN files for the router before recreating them
|
| 16 |
rodolico |
53 |
|
| 14 |
rodolico |
54 |
use strict;
|
|
|
55 |
use warnings;
|
|
|
56 |
|
| 16 |
rodolico |
57 |
# use libraries from the directory this script is in
|
|
|
58 |
BEGIN {
|
|
|
59 |
use FindBin;
|
|
|
60 |
use File::Spec;
|
|
|
61 |
# use libraries from the directory this script is in
|
|
|
62 |
use Cwd 'abs_path';
|
|
|
63 |
use File::Basename;
|
|
|
64 |
use lib dirname( abs_path( __FILE__ ) );
|
|
|
65 |
}
|
| 14 |
rodolico |
66 |
|
| 16 |
rodolico |
67 |
|
| 14 |
rodolico |
68 |
use JSON; # for encode_json, decode_json
|
|
|
69 |
use Data::Dumper; # for debugging
|
|
|
70 |
use opnsense; # our module to handle opnsense API calls
|
|
|
71 |
use GD::Barcode::QRcode; # for creating the QR code images
|
|
|
72 |
use MIME::Base64 qw( decode_base64 ); # for decoding the ovpn file returned from the API
|
|
|
73 |
use File::Path qw( make_path ); # for creating directories
|
| 16 |
rodolico |
74 |
use Fcntl qw(:flock); # Import locking constants
|
|
|
75 |
|
| 21 |
rodolico |
76 |
our $VERSION = '1.1.1';
|
| 16 |
rodolico |
77 |
|
|
|
78 |
# Check if running as root
|
|
|
79 |
if ($> != 0) {
|
|
|
80 |
die "Error: This script must be run as root.\n";
|
|
|
81 |
}
|
|
|
82 |
|
|
|
83 |
#-----------------------------
|
|
|
84 |
# Configuration Variables
|
|
|
85 |
#-----------------------------
|
|
|
86 |
# Location of the configuration files and directories where we will store data
|
|
|
87 |
# relative to the script directory as written
|
|
|
88 |
my $scriptDir = $FindBin::RealBin;
|
|
|
89 |
my $configFile = $scriptDir . '/routers.json';
|
|
|
90 |
my $usersFile = $scriptDir . '/users.json';
|
|
|
91 |
my $qrLocation = './qrcodes';
|
|
|
92 |
my $ovpnFileLocation = './openvpn_configs';
|
|
|
93 |
# size of the QR code modules, basically a magnification factor
|
| 14 |
rodolico |
94 |
my $moduleSize = 10;
|
| 16 |
rodolico |
95 |
# using a lock file to ensure script does not run while another request has it running
|
|
|
96 |
my $lockFile = '/tmp/opnsense.lock';
|
|
|
97 |
my $lockRetryTime = 10; # number of seconds between subsequent tries for a lock
|
|
|
98 |
my $lockRetries = 6; # number of times we will attempt to get a lock before failing
|
| 14 |
rodolico |
99 |
|
| 16 |
rodolico |
100 |
#-----------------------------
|
|
|
101 |
# slurpFile: Reads in the entire contents of a file and returns it as a string
|
|
|
102 |
# $file - name of file to read
|
|
|
103 |
# returns the contents of the file as a string, or empty string if file does not exist
|
|
|
104 |
# since we do this a lot, make it a separate function. This is likely the most efficient way
|
|
|
105 |
# to read a file in Perl
|
|
|
106 |
#-----------------------------
|
|
|
107 |
sub slurpFile {
|
|
|
108 |
my ($file) = @_;
|
|
|
109 |
my $data = '';
|
|
|
110 |
if (-e $file) {
|
|
|
111 |
open(my $fh, '<', $file) or die "Cannot open $file: $!";
|
| 14 |
rodolico |
112 |
local $/; # slurp mode
|
| 16 |
rodolico |
113 |
$data = <$fh>;
|
| 14 |
rodolico |
114 |
close $fh;
|
|
|
115 |
}
|
|
|
116 |
return $data;
|
|
|
117 |
}
|
|
|
118 |
|
| 16 |
rodolico |
119 |
#-----------------------------
|
|
|
120 |
# loadConfig: Load router configuration from config file
|
|
|
121 |
# $file - name of file to read
|
|
|
122 |
# $entry - name of the router entry to load
|
|
|
123 |
# returns a reference to the data structure for the router, or empty hash if not found
|
|
|
124 |
#-----------------------------
|
|
|
125 |
sub loadConfig {
|
|
|
126 |
my ($file, $entry) = @_;
|
|
|
127 |
my $return = &slurpFile($file);
|
|
|
128 |
return {} unless $return && $return ne '';
|
|
|
129 |
my $data = decode_json($return);
|
|
|
130 |
return $data->{$entry} if (exists $data->{$entry});
|
|
|
131 |
return {};
|
|
|
132 |
}
|
|
|
133 |
|
|
|
134 |
#-----------------------------
|
|
|
135 |
# loadUsers: Load users data structure from file
|
|
|
136 |
# $file - name of file to read
|
|
|
137 |
# returns a reference to the data structure
|
|
|
138 |
# $file assumed to be in JSON format
|
|
|
139 |
#-----------------------------
|
|
|
140 |
sub loadUsers {
|
|
|
141 |
my ($file) = @_;
|
|
|
142 |
my $data = &slurpFile($file);
|
|
|
143 |
return {} unless $data && $data ne '';
|
|
|
144 |
my $return = decode_json($data);
|
|
|
145 |
return {} unless $return && ref($return) eq 'HASH';
|
|
|
146 |
return $return;
|
|
|
147 |
}
|
|
|
148 |
|
|
|
149 |
#-----------------------------
|
|
|
150 |
# saveUsers: Save users data structure to file
|
|
|
151 |
# $file - name of file to write
|
|
|
152 |
# $data - reference to the data to write
|
|
|
153 |
# $timeStampFile - optional name of a file to write the current timestamp to
|
|
|
154 |
# writes the data in JSON format
|
|
|
155 |
#-----------------------------
|
| 14 |
rodolico |
156 |
sub saveUsers {
|
| 16 |
rodolico |
157 |
my ($file, $data) = @_;
|
|
|
158 |
open(my $fh, '>', $file) or die "Cannot open $file: $!";
|
| 14 |
rodolico |
159 |
print $fh encode_json($data);
|
|
|
160 |
close $fh;
|
|
|
161 |
}
|
|
|
162 |
|
| 16 |
rodolico |
163 |
#-----------------------------
|
|
|
164 |
# makeQR: Creates a QR code file
|
|
|
165 |
# $routerName - name of the router (used in filename and OTP URL)
|
|
|
166 |
# $account - account name (used in filename and OTP URL)
|
|
|
167 |
# $secret - OTP secret
|
|
|
168 |
# $moduleSize - size of the QR code modules
|
|
|
169 |
# $relativeDir - directory to save the QR code file
|
|
|
170 |
# $absDir - The directory the script is in, so writing relative directories will correctly place
|
|
|
171 |
# returns the filename of the created QR code image
|
|
|
172 |
#-----------------------------
|
| 14 |
rodolico |
173 |
sub makeQR {
|
| 16 |
rodolico |
174 |
# Creates a QR code image for the user's OTP secret
|
|
|
175 |
my ($routerName, $account, $secret, $moduleSize, $relativeDir, $absDir ) = @_;
|
|
|
176 |
# build the OTP URL to include router name as issuer and the account name for display in authenticator apps
|
|
|
177 |
my $otpUrl = "otpauth://totp/$routerName:$account?secret=$secret&issuer=$routerName";
|
|
|
178 |
# generate the QR code
|
|
|
179 |
my $barcode = GD::Barcode::QRcode->new($otpUrl, { Ecc => 'M', ModuleSize => $moduleSize } );
|
| 14 |
rodolico |
180 |
my $image = $barcode->plot();
|
| 16 |
rodolico |
181 |
my $fileName = ($routerName ? $routerName . '_' : '' ) . $account . '.png';
|
|
|
182 |
# This allows us to retain the relative file path for storage, but ensuring we write the file to
|
|
|
183 |
# the correct path even if we are not running the script from the script directory.
|
|
|
184 |
open my $out, '>', "$absDir/" . $fileName or die "Cannot write file $fileName to $absDir: $!";
|
| 14 |
rodolico |
185 |
binmode $out;
|
|
|
186 |
print $out $image->png;
|
|
|
187 |
close $out;
|
| 16 |
rodolico |
188 |
return $relativeDir . '/' . $fileName;
|
| 14 |
rodolico |
189 |
}
|
|
|
190 |
|
| 16 |
rodolico |
191 |
#-----------------------------
|
|
|
192 |
# makeVPNConfigFile: Creates a VPN configuration file for the user
|
|
|
193 |
# $contents - reference to the hash containing the 'content' key with base64 encoded ovpn file
|
| 20 |
rodolico |
194 |
# $routerName - name of the router (used in filename template replacement)
|
|
|
195 |
# $userName - name of the user (used in filename template replacement)
|
| 16 |
rodolico |
196 |
# $ovpnLocation - The relative path to the output directory
|
|
|
197 |
# $absDir - The absolute path to the output directory
|
| 20 |
rodolico |
198 |
# $filenameTemplate - template for filename with ROUTER and USER placeholders
|
|
|
199 |
# $additionalOvpnStrings - additional strings to add to the ovpn file, \n will be expanded to newlines
|
| 16 |
rodolico |
200 |
# returns the filename of the created ovpn file
|
|
|
201 |
#-----------------------------
|
| 14 |
rodolico |
202 |
sub makeVPNConfigFile {
|
| 20 |
rodolico |
203 |
my ($contents, $routerName, $userName, $ovpnLocation, $absDir, $filenameTemplate, $additionalOvpnStrings) = @_;
|
| 14 |
rodolico |
204 |
return '' unless $contents && $contents->{'content'};
|
| 20 |
rodolico |
205 |
# Replace ROUTER and USER in the filename template
|
|
|
206 |
my $fileName = $filenameTemplate;
|
|
|
207 |
$fileName =~ s/ROUTER/$routerName/g;
|
|
|
208 |
$fileName =~ s/USER/$userName/g;
|
| 21 |
rodolico |
209 |
# modified to use a separate variable for the file contents since it was overwritten previously
|
|
|
210 |
my $fileContents = $contents->{'content'};
|
|
|
211 |
$fileContents .= "\n";
|
|
|
212 |
$fileContents =~ s/(\r?\n)+/\n/g; # normalize line endings
|
| 14 |
rodolico |
213 |
my $comment = '';
|
| 21 |
rodolico |
214 |
if ( $fileContents !~ /# Added by opensense-totp-ovpn-export/ ) {
|
|
|
215 |
$comment = "# Added by opensense-totp-ovpn-export\n";
|
| 14 |
rodolico |
216 |
}
|
| 20 |
rodolico |
217 |
# Expand \n to actual newlines in additionalOvpnStrings
|
|
|
218 |
$additionalOvpnStrings =~ s/\\n/\n/g if $additionalOvpnStrings;
|
|
|
219 |
foreach my $string ( split( /\n/, $additionalOvpnStrings || '' ) ) {
|
|
|
220 |
next if $string eq '';
|
| 21 |
rodolico |
221 |
unless ( $fileContents =~ /\Q$string\E/ ) {
|
|
|
222 |
$fileContents .= "$comment$string\n";
|
| 14 |
rodolico |
223 |
$comment = ''; # only add comment once
|
|
|
224 |
}
|
|
|
225 |
}
|
| 16 |
rodolico |
226 |
# This allows us to retain the relative file path for storage, but ensuring we write the file to
|
|
|
227 |
# the correct path even if we are not running the script from the script directory.
|
|
|
228 |
open my $out, '>', "$absDir/" . $fileName or die "Cannot write file $fileName to $absDir: $!";
|
| 21 |
rodolico |
229 |
print $out $fileContents;
|
| 14 |
rodolico |
230 |
close $out;
|
| 16 |
rodolico |
231 |
return $ovpnLocation . '/' . $fileName;
|
| 14 |
rodolico |
232 |
}
|
|
|
233 |
|
| 16 |
rodolico |
234 |
# cleanOldDataFiles: Deletes old data files in the specified directory matching the given pattern
|
|
|
235 |
# $directory - directory to scan for old files
|
|
|
236 |
# $pattern - regex pattern to match files to delete
|
|
|
237 |
# simply removes all files matching the pattern
|
|
|
238 |
#-----------------------------
|
|
|
239 |
sub cleanOldDataFiles {
|
|
|
240 |
my ( $directory, $pattern ) = @_;
|
|
|
241 |
opendir(my $dh, $directory) or die "Cannot open directory $directory: $!";
|
|
|
242 |
while (my $file = readdir($dh)) {
|
|
|
243 |
next if ($file =~ /^\./);
|
|
|
244 |
if ($file =~ /$pattern/) {
|
|
|
245 |
unlink "$directory/$file" or warn "Could not delete $directory/$file: $!";
|
|
|
246 |
}
|
|
|
247 |
}
|
|
|
248 |
closedir($dh);
|
|
|
249 |
}
|
| 14 |
rodolico |
250 |
|
|
|
251 |
|
| 16 |
rodolico |
252 |
#-----------------------------
|
|
|
253 |
# Main program
|
|
|
254 |
#-----------------------------
|
|
|
255 |
|
|
|
256 |
# make sure script does not run more than once at the same time
|
|
|
257 |
# Open the lock file
|
|
|
258 |
open(my $fh, '>', $lockFile) or die "Cannot open lock file $lockFile: $!";
|
|
|
259 |
# Attempt to acquire an exclusive lock. We will try $lockTimeOut times, each
|
|
|
260 |
# waiting $lockRetryTime
|
|
|
261 |
while ( $lockRetries-- ) {
|
|
|
262 |
if (flock($fh, LOCK_EX | LOCK_NB)) {
|
|
|
263 |
last;
|
|
|
264 |
} else {
|
|
|
265 |
sleep($lockRetryTime);
|
|
|
266 |
}
|
|
|
267 |
}
|
|
|
268 |
# if $lockTimeOut is zero, we could not get a lock in, so die
|
|
|
269 |
die "Another instance of the script is already running and retries exceeded!\n" unless ( $lockRetries );
|
|
|
270 |
|
| 14 |
rodolico |
271 |
# get the router name from the command line
|
|
|
272 |
my $router = shift @ARGV or die "Usage: $0 <router_name>\n";
|
|
|
273 |
# Load existing configuration or initialize a new one
|
| 16 |
rodolico |
274 |
my $config = &loadConfig($configFile, $router );
|
|
|
275 |
die "Configuration for router $router not found in $configFile\n" unless $config && ref($config) eq 'HASH';
|
| 20 |
rodolico |
276 |
# die Dumper( $config ) . "\n";
|
| 14 |
rodolico |
277 |
|
|
|
278 |
# load the users file. We will update the entry for this router
|
| 16 |
rodolico |
279 |
my $users = &loadUsers( $usersFile );
|
| 14 |
rodolico |
280 |
|
|
|
281 |
# if there is no entry for this router, create an empty one
|
|
|
282 |
$users->{$router} = {} unless exists $users->{$router};
|
|
|
283 |
$users->{$router}->{'qrLocation'} = $qrLocation unless exists $users->{$router}->{'qrLocation'};
|
|
|
284 |
$users->{$router}->{'ovpnLocation'} = $ovpnFileLocation unless exists $users->{$router}->{'ovpnLocation'};
|
|
|
285 |
$users->{$router}->{'users'} = {}; # clean out the users list, we will reload it
|
| 16 |
rodolico |
286 |
# and get the location on the file system in case we are not running the script from the script directory
|
|
|
287 |
$users->{$router}->{'qrLocationFileystem'} = abs_path( $scriptDir . '/' . $qrLocation )
|
|
|
288 |
unless exists $users->{$router}->{'qrLocationFileystem'};
|
|
|
289 |
$users->{$router}->{'ovpnLocationFileSystem'} = abs_path( $scriptDir . '/' . $ovpnFileLocation )
|
|
|
290 |
unless exists $users->{$router}->{'ovpnLocationFileSystem'};
|
| 18 |
rodolico |
291 |
# finally, copy the download token if it exists
|
|
|
292 |
$users->{$router}->{'downloadToken'} = $config->{'downloadToken'}
|
|
|
293 |
if exists $config->{'downloadToken'};
|
| 14 |
rodolico |
294 |
|
| 18 |
rodolico |
295 |
#die Dumper( $users ) . "\n";
|
| 16 |
rodolico |
296 |
|
| 18 |
rodolico |
297 |
|
| 14 |
rodolico |
298 |
# ensure the directories exist and give them full access
|
| 16 |
rodolico |
299 |
make_path( $users->{$router}->{'qrLocationFileystem'}, 0777 ) unless -d $users->{$router}->{'qrLocationFileystem'};
|
|
|
300 |
make_path( $users->{$router}->{'ovpnLocationFileSystem'}, 0777 ) unless -d $users->{$router}->{'ovpnLocationFileSystem'};
|
| 14 |
rodolico |
301 |
|
| 16 |
rodolico |
302 |
# instead of actually going through and cleaning files no longer in the users list,
|
|
|
303 |
# just delete all files for this router and recreate them
|
|
|
304 |
&cleanOldDataFiles( $users->{$router}->{'qrLocationFileystem'}, qr/^\Q$router\E_.*\.png$/ );
|
|
|
305 |
&cleanOldDataFiles( $users->{$router}->{'ovpnLocationFileSystem'}, qr/^\Q$router\E_.*\.ovpn$/ );
|
|
|
306 |
|
| 14 |
rodolico |
307 |
# this does most of the work, all the API calls are handled in the module
|
|
|
308 |
# create the opnsense object
|
| 16 |
rodolico |
309 |
my $opnsense = opnsense->new(
|
| 14 |
rodolico |
310 |
url => $config->{'url'},
|
|
|
311 |
apiKey => $config->{'apiKey'},
|
|
|
312 |
apiSecret => $config->{'apiSecret'},
|
|
|
313 |
ovpnIndex => $config->{'ovpnIndex'},
|
| 16 |
rodolico |
314 |
localPort => $config->{'localPort'},
|
| 14 |
rodolico |
315 |
template => $config->{'template'},
|
|
|
316 |
hostname => $config->{'hostname'},
|
|
|
317 |
);
|
|
|
318 |
|
|
|
319 |
# get the VPN users. This is a hashref keyed by cert name, value is username
|
| 16 |
rodolico |
320 |
my $vpnCerts = $opnsense->getVpnUsers();
|
| 17 |
rodolico |
321 |
#die Dumper($vpnCerts);
|
| 14 |
rodolico |
322 |
# convert the cert-"user" to username->certs, array ref of certs as not sure if multiple certs per user allowed
|
|
|
323 |
foreach my $cert ( keys %$vpnCerts ) {
|
|
|
324 |
push @{$users->{$router}->{'users'}->{$vpnCerts->{$cert}}->{'certs'}}, $cert;
|
|
|
325 |
}
|
|
|
326 |
|
|
|
327 |
# these are all of the users on the system
|
| 16 |
rodolico |
328 |
my $allUsers = $opnsense->getAllUsers();
|
| 14 |
rodolico |
329 |
|
|
|
330 |
# for each user in the system, if they are in the vpnUsers list, copy otp_seed, cert, and password
|
|
|
331 |
# we'll also delete any users who are disabled
|
|
|
332 |
my @keys = ('otp_seed', 'cert', 'password' ); # these are the keys we want to copy
|
|
|
333 |
foreach my $user ( keys %$allUsers ) {
|
|
|
334 |
next unless exists $users->{$router}->{'users'}->{$user}; # skip users who do not have vpn certs
|
|
|
335 |
if ( $allUsers->{$user}->{'disabled'} ) { # skip users who are disabled
|
|
|
336 |
delete $users->{$router}->{'users'}->{$user};
|
|
|
337 |
next;
|
|
|
338 |
}
|
|
|
339 |
foreach my $key ( @keys ) {
|
|
|
340 |
$users->{$router}->{'users'}->{$user}->{$key} = ref($allUsers->{$user}->{$key}) ? '' : $allUsers->{$user}->{$key};
|
|
|
341 |
}
|
|
|
342 |
}
|
|
|
343 |
|
|
|
344 |
# create the QR code file and VPN configuration for each user
|
|
|
345 |
foreach my $entry ( keys %{$users->{$router}->{'users'}} ) {
|
|
|
346 |
my $account = $entry;
|
| 16 |
rodolico |
347 |
my $secret = $users->{$router}->{'users'}->{$entry}->{'otp_seed'} || '';
|
|
|
348 |
# Create the QR code file and store the filename in the data structure
|
|
|
349 |
# If there is no secret, do not create a QR code
|
| 14 |
rodolico |
350 |
$users->{$router}->{'users'}->{$entry}->{'qrFile'} =
|
| 16 |
rodolico |
351 |
$secret && $secret ne '' ?
|
|
|
352 |
&makeQR(
|
|
|
353 |
$router,
|
|
|
354 |
$account,
|
|
|
355 |
$secret,
|
|
|
356 |
$moduleSize,
|
|
|
357 |
$users->{$router}->{'qrLocation'},
|
|
|
358 |
$users->{$router}->{'qrLocationFileystem'}
|
|
|
359 |
) :
|
|
|
360 |
'';
|
|
|
361 |
|
| 20 |
rodolico |
362 |
# Create the VPN configuration file(s), if they exist. If multiple certs, only use the first one
|
| 16 |
rodolico |
363 |
# warn user if there are multiple certs
|
| 14 |
rodolico |
364 |
if ( scalar(@{$users->{$router}->{'users'}->{$entry}->{'certs'}}) > 1 ) {
|
|
|
365 |
warn "$entry has multiple certs, using first one only\n";
|
|
|
366 |
}
|
| 16 |
rodolico |
367 |
my $cert = $opnsense->getVpnConfig( $users->{$router}->{'users'}->{$entry}->{'certs'}->[0] );
|
| 14 |
rodolico |
368 |
if ( !$cert || ref($cert) ne 'HASH' || !exists $cert->{'content'} ) {
|
|
|
369 |
warn "Could not get VPN configuration for $entry, cert $users->{$router}->{'users'}->{$entry}->{'certs'}->[0]\n";
|
|
|
370 |
next;
|
|
|
371 |
}
|
| 20 |
rodolico |
372 |
|
|
|
373 |
# Process formats to create multiple OVPN files if formats are defined
|
|
|
374 |
my @ovpnFiles = ();
|
|
|
375 |
my $formats = $config->{'formats'};
|
|
|
376 |
# die Dumper($config) . "\n";
|
|
|
377 |
# die Dumper($formats) . "\n";
|
|
|
378 |
if ( $formats && ref($formats) eq 'HASH' && keys %$formats ) {
|
|
|
379 |
# Iterate through each format
|
| 21 |
rodolico |
380 |
$cert->{'content'} = decode_base64( $cert->{'content'} ); # decode once for all formats
|
| 20 |
rodolico |
381 |
foreach my $formatName ( keys %$formats ) {
|
|
|
382 |
my $format = $formats->{$formatName};
|
|
|
383 |
next unless ref($format) eq 'HASH';
|
|
|
384 |
my $filename = $format->{'filename'} || '';
|
|
|
385 |
my $additionalStrings = $format->{'additionalStrings'} || '';
|
|
|
386 |
next if $filename eq '';
|
|
|
387 |
my $ovpnFile = &makeVPNConfigFile(
|
|
|
388 |
$cert,
|
|
|
389 |
$router,
|
|
|
390 |
$entry,
|
|
|
391 |
$users->{$router}->{'ovpnLocation'},
|
|
|
392 |
$users->{$router}->{'ovpnLocationFileSystem'},
|
|
|
393 |
$filename,
|
|
|
394 |
$additionalStrings
|
|
|
395 |
);
|
|
|
396 |
push @ovpnFiles, $ovpnFile if $ovpnFile;
|
|
|
397 |
}
|
|
|
398 |
}
|
|
|
399 |
|
|
|
400 |
# If no formats defined or no files created, create a default file
|
|
|
401 |
if ( scalar(@ovpnFiles) == 0 ) {
|
|
|
402 |
my $defaultFilename = $router . '_' . $entry . '.ovpn';
|
|
|
403 |
my $ovpnFile = &makeVPNConfigFile(
|
| 16 |
rodolico |
404 |
$cert,
|
|
|
405 |
$router,
|
|
|
406 |
$entry,
|
|
|
407 |
$users->{$router}->{'ovpnLocation'},
|
|
|
408 |
$users->{$router}->{'ovpnLocationFileSystem'},
|
| 20 |
rodolico |
409 |
$defaultFilename,
|
|
|
410 |
"static-challenge 'Enter Auth Code' 0\nauth-nocache"
|
|
|
411 |
);
|
|
|
412 |
push @ovpnFiles, $ovpnFile if $ovpnFile;
|
|
|
413 |
}
|
|
|
414 |
|
|
|
415 |
# Store as array if multiple files, or single string if only one
|
|
|
416 |
$users->{$router}->{'users'}->{$entry}->{'ovpnFile'} =
|
|
|
417 |
scalar(@ovpnFiles) > 1 ? \@ovpnFiles : $ovpnFiles[0];
|
| 14 |
rodolico |
418 |
}
|
| 16 |
rodolico |
419 |
# update the timestamp for the current router
|
|
|
420 |
$users->{$router}->{'lastUpdate'} = time;
|
|
|
421 |
# save the users file
|
|
|
422 |
&saveUsers( $usersFile, $users );
|
| 14 |
rodolico |
423 |
|
| 16 |
rodolico |
424 |
# Remove the lock file so the script can run again
|
|
|
425 |
close($fh);
|
|
|
426 |
unlink($lockFile) or warn "Could not unlink lock file: $!";
|
|
|
427 |
|
|
|
428 |
1;
|