| Line 37... |
Line 37... |
| 37 |
# Added a lock file to not allow the script to run if it is already running. If we can not
|
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
|
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
|
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)
|
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)
|
41 |
# location to work easily (less code)
|
| - |
|
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
|
| 42 |
|
49 |
|
| 43 |
use strict;
|
50 |
use strict;
|
| 44 |
use warnings;
|
51 |
use warnings;
|
| 45 |
|
52 |
|
| 46 |
# use libraries from the directory this script is in
|
53 |
# use libraries from the directory this script is in
|
| Line 60... |
Line 67... |
| 60 |
use GD::Barcode::QRcode; # for creating the QR code images
|
67 |
use GD::Barcode::QRcode; # for creating the QR code images
|
| 61 |
use MIME::Base64 qw( decode_base64 ); # for decoding the ovpn file returned from the API
|
68 |
use MIME::Base64 qw( decode_base64 ); # for decoding the ovpn file returned from the API
|
| 62 |
use File::Path qw( make_path ); # for creating directories
|
69 |
use File::Path qw( make_path ); # for creating directories
|
| 63 |
use Fcntl qw(:flock); # Import locking constants
|
70 |
use Fcntl qw(:flock); # Import locking constants
|
| 64 |
|
71 |
|
| 65 |
our $VERSION = '1.0.1';
|
72 |
our $VERSION = '1.1.0';
|
| 66 |
|
73 |
|
| 67 |
# Check if running as root
|
74 |
# Check if running as root
|
| 68 |
if ($> != 0) {
|
75 |
if ($> != 0) {
|
| 69 |
die "Error: This script must be run as root.\n";
|
76 |
die "Error: This script must be run as root.\n";
|
| 70 |
}
|
77 |
}
|
| Line 79... |
Line 86... |
| 79 |
my $usersFile = $scriptDir . '/users.json';
|
86 |
my $usersFile = $scriptDir . '/users.json';
|
| 80 |
my $qrLocation = './qrcodes';
|
87 |
my $qrLocation = './qrcodes';
|
| 81 |
my $ovpnFileLocation = './openvpn_configs';
|
88 |
my $ovpnFileLocation = './openvpn_configs';
|
| 82 |
# size of the QR code modules, basically a magnification factor
|
89 |
# size of the QR code modules, basically a magnification factor
|
| 83 |
my $moduleSize = 10;
|
90 |
my $moduleSize = 10;
|
| 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
|
91 |
# using a lock file to ensure script does not run while another request has it running
|
| 87 |
my $lockFile = '/tmp/opnsense.lock';
|
92 |
my $lockFile = '/tmp/opnsense.lock';
|
| 88 |
my $lockRetryTime = 10; # number of seconds between subsequent tries for a lock
|
93 |
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
|
94 |
my $lockRetries = 6; # number of times we will attempt to get a lock before failing
|
| 90 |
|
95 |
|
| Line 180... |
Line 185... |
| 180 |
}
|
185 |
}
|
| 181 |
|
186 |
|
| 182 |
#-----------------------------
|
187 |
#-----------------------------
|
| 183 |
# makeVPNConfigFile: Creates a VPN configuration file for the user
|
188 |
# makeVPNConfigFile: Creates a VPN configuration file for the user
|
| 184 |
# $contents - reference to the hash containing the 'content' key with base64 encoded ovpn file
|
189 |
# $contents - reference to the hash containing the 'content' key with base64 encoded ovpn file
|
| 185 |
# $routerName - name of the router (used in filename)
|
190 |
# $routerName - name of the router (used in filename template replacement)
|
| 186 |
# $userName - name of the user (used in filename)
|
191 |
# $userName - name of the user (used in filename template replacement)
|
| 187 |
# $ovpnLocation - The relative path to the output directory
|
192 |
# $ovpnLocation - The relative path to the output directory
|
| 188 |
# $absDir - The absolute path to the output directory
|
193 |
# $absDir - The absolute path to the output directory
|
| - |
|
194 |
# $filenameTemplate - template for filename with ROUTER and USER placeholders
|
| 189 |
# $additionalOvpnStrings - additional strings to add to the ovpn file if not already present
|
195 |
# $additionalOvpnStrings - additional strings to add to the ovpn file, \n will be expanded to newlines
|
| 190 |
# returns the filename of the created ovpn file
|
196 |
# returns the filename of the created ovpn file
|
| 191 |
#-----------------------------
|
197 |
#-----------------------------
|
| 192 |
sub makeVPNConfigFile {
|
198 |
sub makeVPNConfigFile {
|
| 193 |
my ($contents, $routerName, $userName, $ovpnLocation, $absDir, $additionalOvpnStrings) = @_;
|
199 |
my ($contents, $routerName, $userName, $ovpnLocation, $absDir, $filenameTemplate, $additionalOvpnStrings) = @_;
|
| 194 |
return '' unless $contents && $contents->{'content'};
|
200 |
return '' unless $contents && $contents->{'content'};
|
| - |
|
201 |
# Replace ROUTER and USER in the filename template
|
| 195 |
my $fileName = ($routerName ? $routerName . '_' : '' ) . $userName . '.ovpn';
|
202 |
my $fileName = $filenameTemplate;
|
| - |
|
203 |
$fileName =~ s/ROUTER/$routerName/g;
|
| - |
|
204 |
$fileName =~ s/USER/$userName/g;
|
| 196 |
$contents->{'content'} = decode_base64($contents->{'content'});
|
205 |
$contents->{'content'} = decode_base64($contents->{'content'});
|
| 197 |
$contents->{'content'} .= "\n";
|
206 |
$contents->{'content'} .= "\n";
|
| 198 |
$contents->{'content'} =~ s/(\r?\n)+/\n/g; # normalize line endings
|
207 |
$contents->{'content'} =~ s/(\r?\n)+/\n/g; # normalize line endings
|
| 199 |
my $comment = '';
|
208 |
my $comment = '';
|
| 200 |
if ( $contents->{'content'} !~ /# Added by loadOpnSense.pl/ ) {
|
209 |
if ( $contents->{'content'} !~ /# Added by loadOpnSense.pl/ ) {
|
| 201 |
$comment = "# Added by loadOpnSense.pl\n";
|
210 |
$comment = "# Added by loadOpnSense.pl\n";
|
| 202 |
}
|
211 |
}
|
| - |
|
212 |
# Expand \n to actual newlines in additionalOvpnStrings
|
| - |
|
213 |
$additionalOvpnStrings =~ s/\\n/\n/g if $additionalOvpnStrings;
|
| 203 |
foreach my $string ( split( /\n/, $additionalOvpnStrings ) ) {
|
214 |
foreach my $string ( split( /\n/, $additionalOvpnStrings || '' ) ) {
|
| - |
|
215 |
next if $string eq '';
|
| 204 |
unless ( $contents->{'content'} =~ /\$string/ ) {
|
216 |
unless ( $contents->{'content'} =~ /\Q$string\E/ ) {
|
| 205 |
$contents->{'content'} .= "$comment$string\n";
|
217 |
$contents->{'content'} .= "$comment$string\n";
|
| 206 |
$comment = ''; # only add comment once
|
218 |
$comment = ''; # only add comment once
|
| 207 |
}
|
219 |
}
|
| 208 |
}
|
220 |
}
|
| 209 |
# This allows us to retain the relative file path for storage, but ensuring we write the file to
|
221 |
# This allows us to retain the relative file path for storage, but ensuring we write the file to
|
| Line 254... |
Line 266... |
| 254 |
# get the router name from the command line
|
266 |
# get the router name from the command line
|
| 255 |
my $router = shift @ARGV or die "Usage: $0 <router_name>\n";
|
267 |
my $router = shift @ARGV or die "Usage: $0 <router_name>\n";
|
| 256 |
# Load existing configuration or initialize a new one
|
268 |
# Load existing configuration or initialize a new one
|
| 257 |
my $config = &loadConfig($configFile, $router );
|
269 |
my $config = &loadConfig($configFile, $router );
|
| 258 |
die "Configuration for router $router not found in $configFile\n" unless $config && ref($config) eq 'HASH';
|
270 |
die "Configuration for router $router not found in $configFile\n" unless $config && ref($config) eq 'HASH';
|
| - |
|
271 |
# die Dumper( $config ) . "\n";
|
| 259 |
|
272 |
|
| 260 |
# load the users file. We will update the entry for this router
|
273 |
# load the users file. We will update the entry for this router
|
| 261 |
my $users = &loadUsers( $usersFile );
|
274 |
my $users = &loadUsers( $usersFile );
|
| 262 |
|
275 |
|
| 263 |
# if there is no entry for this router, create an empty one
|
276 |
# if there is no entry for this router, create an empty one
|
| Line 339... |
Line 352... |
| 339 |
$users->{$router}->{'qrLocation'},
|
352 |
$users->{$router}->{'qrLocation'},
|
| 340 |
$users->{$router}->{'qrLocationFileystem'}
|
353 |
$users->{$router}->{'qrLocationFileystem'}
|
| 341 |
) :
|
354 |
) :
|
| 342 |
'';
|
355 |
'';
|
| 343 |
|
356 |
|
| 344 |
# Create the VPN configuration file, if it exists. If multiple certs, only create the first one
|
357 |
# Create the VPN configuration file(s), if they exist. If multiple certs, only use the first one
|
| 345 |
# warn user if there are multiple certs
|
358 |
# warn user if there are multiple certs
|
| 346 |
if ( scalar(@{$users->{$router}->{'users'}->{$entry}->{'certs'}}) > 1 ) {
|
359 |
if ( scalar(@{$users->{$router}->{'users'}->{$entry}->{'certs'}}) > 1 ) {
|
| 347 |
warn "$entry has multiple certs, using first one only\n";
|
360 |
warn "$entry has multiple certs, using first one only\n";
|
| 348 |
}
|
361 |
}
|
| 349 |
my $cert = $opnsense->getVpnConfig( $users->{$router}->{'users'}->{$entry}->{'certs'}->[0] );
|
362 |
my $cert = $opnsense->getVpnConfig( $users->{$router}->{'users'}->{$entry}->{'certs'}->[0] );
|
| 350 |
if ( !$cert || ref($cert) ne 'HASH' || !exists $cert->{'content'} ) {
|
363 |
if ( !$cert || ref($cert) ne 'HASH' || !exists $cert->{'content'} ) {
|
| 351 |
warn "Could not get VPN configuration for $entry, cert $users->{$router}->{'users'}->{$entry}->{'certs'}->[0]\n";
|
364 |
warn "Could not get VPN configuration for $entry, cert $users->{$router}->{'users'}->{$entry}->{'certs'}->[0]\n";
|
| 352 |
next;
|
365 |
next;
|
| 353 |
}
|
366 |
}
|
| - |
|
367 |
|
| - |
|
368 |
# Process formats to create multiple OVPN files if formats are defined
|
| - |
|
369 |
my @ovpnFiles = ();
|
| - |
|
370 |
my $formats = $config->{'formats'};
|
| - |
|
371 |
# die Dumper($config) . "\n";
|
| - |
|
372 |
# die Dumper($formats) . "\n";
|
| - |
|
373 |
if ( $formats && ref($formats) eq 'HASH' && keys %$formats ) {
|
| - |
|
374 |
# Iterate through each format
|
| - |
|
375 |
foreach my $formatName ( keys %$formats ) {
|
| - |
|
376 |
my $format = $formats->{$formatName};
|
| - |
|
377 |
next unless ref($format) eq 'HASH';
|
| - |
|
378 |
my $filename = $format->{'filename'} || '';
|
| - |
|
379 |
my $additionalStrings = $format->{'additionalStrings'} || '';
|
| - |
|
380 |
next if $filename eq '';
|
| - |
|
381 |
my $ovpnFile = &makeVPNConfigFile(
|
| - |
|
382 |
$cert,
|
| - |
|
383 |
$router,
|
| - |
|
384 |
$entry,
|
| - |
|
385 |
$users->{$router}->{'ovpnLocation'},
|
| 354 |
$users->{$router}->{'users'}->{$entry}->{'ovpnFile'} =
|
386 |
$users->{$router}->{'ovpnLocationFileSystem'},
|
| - |
|
387 |
$filename,
|
| - |
|
388 |
$additionalStrings
|
| - |
|
389 |
);
|
| - |
|
390 |
push @ovpnFiles, $ovpnFile if $ovpnFile;
|
| - |
|
391 |
}
|
| - |
|
392 |
}
|
| - |
|
393 |
|
| - |
|
394 |
# If no formats defined or no files created, create a default file
|
| - |
|
395 |
if ( scalar(@ovpnFiles) == 0 ) {
|
| - |
|
396 |
my $defaultFilename = $router . '_' . $entry . '.ovpn';
|
| 355 |
&makeVPNConfigFile(
|
397 |
my $ovpnFile = &makeVPNConfigFile(
|
| 356 |
$cert,
|
398 |
$cert,
|
| 357 |
$router,
|
399 |
$router,
|
| 358 |
$entry,
|
400 |
$entry,
|
| 359 |
$users->{$router}->{'ovpnLocation'},
|
401 |
$users->{$router}->{'ovpnLocation'},
|
| 360 |
$users->{$router}->{'ovpnLocationFileSystem'},
|
402 |
$users->{$router}->{'ovpnLocationFileSystem'},
|
| 361 |
$additionalOvpnStrings );
|
403 |
$defaultFilename,
|
| - |
|
404 |
"static-challenge 'Enter Auth Code' 0\nauth-nocache"
|
| - |
|
405 |
);
|
| - |
|
406 |
push @ovpnFiles, $ovpnFile if $ovpnFile;
|
| - |
|
407 |
}
|
| - |
|
408 |
|
| - |
|
409 |
# Store as array if multiple files, or single string if only one
|
| - |
|
410 |
$users->{$router}->{'users'}->{$entry}->{'ovpnFile'} =
|
| - |
|
411 |
scalar(@ovpnFiles) > 1 ? \@ovpnFiles : $ovpnFiles[0];
|
| 362 |
}
|
412 |
}
|
| 363 |
# update the timestamp for the current router
|
413 |
# update the timestamp for the current router
|
| 364 |
$users->{$router}->{'lastUpdate'} = time;
|
414 |
$users->{$router}->{'lastUpdate'} = time;
|
| 365 |
# save the users file
|
415 |
# save the users file
|
| 366 |
&saveUsers( $usersFile, $users );
|
416 |
&saveUsers( $usersFile, $users );
|