Rev 15 | Blame | Compare with Previous | Last modification | View Log | Download | RSS feed
#! /usr/bin/env perl
# Copyright (c) 2025, Daily Data, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this list
# of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright notice, this
# list of conditions and the following disclaimer in the documentation and/or other
# materials provided with the distribution.
# 3. Neither the name of Daily Data, Inc. nor the names of its contributors may be
# used to endorse or promote products derived from this software without specific
# prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
# SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
# TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
# DAMAGE.
#
# This script defines the routers that can be managed by loadOpnSense.pl
# It allows adding, editing, and removing router configurations stored in a JSON file.
#
# Change History:
# v1.0.0 2025-10-01 RWR
# Initial version
use strict;
use warnings;
# use libraries from the directory this script is in
BEGIN {
use FindBin;
use File::Spec;
# use libraries from the directory this script is in
use Cwd 'abs_path';
use File::Basename;
use lib dirname( abs_path( __FILE__ ) );
}
use JSON qw( decode_json encode_json );
use Data::Dumper;
use opnsense;
our $VERSION = "1.0.0";
# Default config file name if not specified on command line. Defaults to 'routers.json' in the script directory
my $configFileName = $FindBin::RealBin . '/routers.json';
# fields used in the router configuration
my @fields = ( 'url', 'apiKey', 'apiSecret', 'ovpnIndex', 'template', 'hostname', 'localPort' );
# Check if running as root
if ($> != 0) {
die "Error: This script must be run as root.\n";
}
#-----------------------------
# usage: Print usage message
#-----------------------------
sub usage {
print "Usage: $0 <opnsenseConfigFile>\n";
exit 1;
}
#-----------------------------
# readConfig: Reads in the configuration file and returns a hash ref of the data
# If file does not exist, returns empty hash ref
# file - name of file to read (default $configFileName)
#-----------------------------
sub readConfig {
my ($file) = @_;
my $data = {};
if (-e $file) {
open(my $fh, '<', $file) or die "Cannot open $file: $!";
local $/; # slurp mode
my $jsonText = <$fh>;
close $fh;
$data = decode_json($jsonText);
}
return $data;
}
#-----------------------------
# writeConfig: Writes the given data to the specified file in JSON format
# file - name of file to write (default $configFileName)
# data - reference to the data to write
# permissions on the file are set to 0600 and ownership to root
# since it contains sensitive information
#-----------------------------
sub writeConfig {
my ( $file, $data ) = @_;
open( my $fh, '>', $file ) or die "Cannot open $file: $!";
print $fh encode_json($data);
close $fh;
chmod 0600, $file or warn "Could not set permissions on $file: $!";
chown 0, 0, $file or warn "Could not set ownership to root on $file: $!";
}
#-----------------------------
# trim: Remove leading and trailing whitespace from a string
# s - the string to trim
#-----------------------------
sub trim {
my ($s) = @_;
$s =~ s/^\s+|\s+$//g if defined $s;
return $s;
}
#-----------------------------
# menuSelect: Display a menu of options and prompt the user to select one
# prompt - the prompt to display to the user
# options - reference to an array of option strings
# additionalText - reference to an array of additional text strings to display next to each option
# returns the selected option string (1-based index)
#-----------------------------
sub menuSelect {
my ( $prompt, $options, $additionalText ) = @_;
print "$prompt\n";
for my $i ( 0 .. $#$options ) {
print " " . ($i+1) . ". " . $options->[$i] . ($additionalText->[$i] ? $additionalText->[$i] : "") . "\n";
}
print "Select option (1-" . ($#$options + 1) . "): ";
my $sel = <STDIN>;
chomp $sel;
if ( $sel !~ /^\d+$/ || $sel < 1 || $sel > ($#$options + 1) ) {
warn "Invalid selection\n";
return menuSelect( $prompt, $options, $additionalText );
}
return $options->[$sel - 1];
}
#-----------------------------
# selectOvpnIndex: Prompts the user to select an ovpnIndex from the list
# in the OPNsense router specified in the config hash
# config - reference to the router's configuration hash
# returns the selected ovpnIndex string or undef if none selected
#-----------------------------
sub selectOvpnIndex {
my ( $config ) = @_;
if ( defined $config->{'url'} && defined $config->{'apiKey'} && defined $config->{'apiSecret'} ) {
my $opnsense = new opnsense(
url => $config->{'url'},
apiKey => $config->{'apiKey'},
apiSecret => $config->{'apiSecret'},
);
my $providers = $opnsense->getVpnProviders();
if ( ref($providers) eq 'HASH' && keys %$providers ) {
my @providerIndex = sort keys %$providers;
my @additionalText = map {
defined $config->{'ovpnIndex'} &&
($_ eq $config->{'ovpnIndex'} ? " (current) " : " - ") . $providers->{$_}
} @providerIndex;
my $selectedProvider = &menuSelect( "Select VPN Provider", \@providerIndex, \@additionalText );
return $selectedProvider;
} else {
warn "No VPN providers found or error retrieving providers. Please check your connection details.\n";
}
} else {
warn "Please set url, apiKey, and apiSecret before selecting ovpnIndex.\n";
}
return undef;
}
#-----------------------------
# editRouter: Edit the configuration for a router
# config - reference to the router's configuration hash
# routerName - name of the router being edited
# keys - reference to an array of keys (fields) that can be edited
# works on one router at a time, allows setting fields for that router
# returns true if configuration was saved, false if not
#-----------------------------
sub editRouter {
my ( $config, $routerName, $keys ) = @_;
my $option = '';
my $saved = 1;
my $currentRouter = $config->{$routerName};
$currentRouter->{'template'} = 'PlainOpenVPN' unless defined $currentRouter->{'template'};
my $vpnProviders = ();
while ( $option ne 'Done' ) {
print "\nEditing router configuration for $routerName" . ($saved ? "" : " (Changed)") . ":\n";
$option = &menuSelect(
"Select field to edit",
[ @$keys, 'Save Config', 'Done' ],
[ map { defined $currentRouter->{$_} ? " (current: $currentRouter->{$_})" : " (not set)" } @$keys, '' ]
);
if ( $option eq 'ovpnIndex' ) {
$currentRouter->{'ovpnIndex'} = &selectOvpnIndex( $currentRouter );
$saved = 0; # mark as unsaved
} elsif ( $option eq 'Save Config' ) {
writeConfig( $configFileName, $config );
$saved = 1; # mark as saved
print "Configuration saved to $configFileName\n";
} elsif ($option eq 'Done') {
$option = &menuSelect( "Exiting configuration editor, but changes were not saved.", [ 'Exit', 'Save and Exit' ] );
if ( $option eq 'Save and Exit' ) {
writeConfig( $configFileName, $config );
$saved = 1; # mark as saved
print "Configuration saved to $configFileName\n";
}
last;
} else {
$saved = 0; # mark as unsaved
print "Enter value for $option: ";
my $value = <STDIN>;
chomp $value;
$value = trim($value);
$currentRouter->{$option} = $value if $value ne '';
}
}
return $saved;
}
$configFileName = shift if @ARGV;
# read the configuration file, if it exists, and place hash in $config
my $config = &readConfig( $configFileName );
# display menu of existing routers plus option to add new router
my $routerName = &menuSelect( "Select router to configure", [ keys %$config, 'Add new router' ] );
# adding a new router, so get the name, then create an empty hashref for it
if ( $routerName eq 'Add new router' ) {
print "Enter new router name: ";
$routerName = <STDIN>;
chomp $routerName;
$routerName = trim($routerName);
$config->{$routerName} = {};
}
# edit the router configuration
&editRouter( $config, $routerName, \@fields );
1;
Generated by GNU Enscript 1.6.5.90.