Subversion Repositories web_pages

Rev

Rev 18 | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
14 rodolico 1
#! /usr/bin/env perl
16 rodolico 2
 
14 rodolico 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.
16 rodolico 28
#
29
# This script defines the routers that can be managed by loadOpnSense.pl
30
# It allows adding, editing, and removing router configurations stored in a JSON file.
31
#
32
# Change History:
33
#  v1.0.0 2025-10-01 RWR
34
#     Initial version 
20 rodolico 35
#  v1.1.0 2026-01-04 RWR
36
#     Added formats key to router configuration
37
#     Added editFormats subroutine to manage formats hash structure
38
#     Formats contain arrays of hashes with filename and additionalStrings keys
14 rodolico 39
 
40
use strict;
41
use warnings;
42
 
16 rodolico 43
# use libraries from the directory this script is in
44
BEGIN {
45
   use FindBin;
46
   use File::Spec;
47
   # use libraries from the directory this script is in
48
   use Cwd 'abs_path';
49
   use File::Basename;
50
   use lib dirname( abs_path( __FILE__ ) );
51
}
52
 
14 rodolico 53
use JSON qw( decode_json encode_json );
54
use Data::Dumper;
55
use opnsense;
56
 
20 rodolico 57
our $VERSION = "1.1.0";
14 rodolico 58
 
16 rodolico 59
# Default config file name if not specified on command line. Defaults to 'routers.json' in the script directory
60
my $configFileName = $FindBin::RealBin . '/routers.json';
61
# fields used in the router configuration
20 rodolico 62
my @fields = ( 'url', 'apiKey', 'apiSecret', 'ovpnIndex', 'template', 'hostname', 'localPort', 'downloadToken', 'formats' );
16 rodolico 63
 
64
# Check if running as root
65
if ($> != 0) {
66
   die "Error: This script must be run as root.\n";
67
}
68
 
69
#-----------------------------
70
# usage: Print usage message
71
#-----------------------------
14 rodolico 72
sub usage {
16 rodolico 73
   print "Usage: $0 <opnsenseConfigFile>\n";
14 rodolico 74
   exit 1;
75
}
76
 
16 rodolico 77
#-----------------------------
78
# readConfig: Reads in the configuration file and returns a hash ref of the data
79
# If file does not exist, returns empty hash ref
80
# file - name of file to read (default $configFileName)
81
#-----------------------------
14 rodolico 82
sub readConfig {
16 rodolico 83
   my ($file) = @_;
14 rodolico 84
   my $data = {};
16 rodolico 85
   if (-e $file) {
86
      open(my $fh, '<', $file) or die "Cannot open $file: $!";
14 rodolico 87
      local $/;  # slurp mode
16 rodolico 88
      my $jsonText = <$fh>;
14 rodolico 89
      close $fh;
16 rodolico 90
      $data = decode_json($jsonText);
14 rodolico 91
   }
92
   return $data;
93
}
94
 
16 rodolico 95
#-----------------------------
96
# writeConfig: Writes the given data to the specified file in JSON format
97
# file - name of file to write (default $configFileName)
98
# data - reference to the data to write
99
# permissions on the file are set to 0600 and ownership to root
100
# since it contains sensitive information
101
#-----------------------------
14 rodolico 102
sub writeConfig {
103
   my ( $file, $data ) = @_;
104
   open( my $fh, '>', $file ) or die "Cannot open $file: $!";
105
   print $fh encode_json($data);
106
   close $fh;
16 rodolico 107
   chmod 0600, $file or warn "Could not set permissions on $file: $!";
108
   chown 0, 0, $file or warn "Could not set ownership to root on $file: $!";
14 rodolico 109
}
110
 
16 rodolico 111
#-----------------------------
112
# trim: Remove leading and trailing whitespace from a string
113
# s - the string to trim
114
#-----------------------------
14 rodolico 115
sub trim {
116
   my ($s) = @_;
117
   $s =~ s/^\s+|\s+$//g if defined $s;
118
   return $s;
119
}
120
 
16 rodolico 121
#-----------------------------
122
# menuSelect: Display a menu of options and prompt the user to select one
123
# prompt - the prompt to display to the user
124
# options - reference to an array of option strings
125
# additionalText - reference to an array of additional text strings to display next to each option
126
# returns the selected option string (1-based index)
127
#-----------------------------
14 rodolico 128
sub menuSelect {
129
   my ( $prompt, $options, $additionalText ) = @_;
130
   print "$prompt\n";
131
   for my $i ( 0 .. $#$options ) {
132
      print "  " . ($i+1) . ". " . $options->[$i] . ($additionalText->[$i] ? $additionalText->[$i] : "") . "\n";
133
   }
134
   print "Select option (1-" . ($#$options + 1) . "): ";
135
   my $sel = <STDIN>;
136
   chomp $sel;
137
   if ( $sel !~ /^\d+$/ || $sel < 1 || $sel > ($#$options + 1) ) {
16 rodolico 138
      warn "Invalid selection\n";
139
      return menuSelect( $prompt, $options, $additionalText );
14 rodolico 140
   }
141
   return $options->[$sel - 1];
142
}
143
 
16 rodolico 144
#-----------------------------
145
# selectOvpnIndex: Prompts the user to select an ovpnIndex from the list
146
# in the OPNsense router specified in the config hash
147
# config - reference to the router's configuration hash
148
# returns the selected ovpnIndex string or undef if none selected
149
#-----------------------------
150
sub selectOvpnIndex {
151
   my ( $config ) = @_;
152
   if ( defined $config->{'url'} && defined $config->{'apiKey'} && defined $config->{'apiSecret'} ) {
153
      my $opnsense = new opnsense(
154
         url    => $config->{'url'},
155
         apiKey    => $config->{'apiKey'},
156
         apiSecret => $config->{'apiSecret'},
157
      );
158
      my $providers = $opnsense->getVpnProviders();
159
      if ( ref($providers) eq 'HASH' && keys %$providers ) {
160
         my @providerIndex = sort keys %$providers;
161
         my @additionalText = map { 
162
               defined $config->{'ovpnIndex'} &&
163
               ($_ eq $config->{'ovpnIndex'} ? " (current) " : " - ") . $providers->{$_} 
164
               } @providerIndex;
165
         my $selectedProvider = &menuSelect( "Select VPN Provider", \@providerIndex, \@additionalText );
166
         return $selectedProvider;
167
      } else {
168
         warn "No VPN providers found or error retrieving providers. Please check your connection details.\n";
169
      }
170
   } else {
171
      warn "Please set url, apiKey, and apiSecret before selecting ovpnIndex.\n";
172
   }
173
   return undef;
174
}
175
 
176
#-----------------------------
20 rodolico 177
# editFormats: Edit the formats hash for a router
178
# formats - reference to the router's formats hash
179
# Each format is a hash key pointing to a hashref with 'filename' and 'additionalStrings' keys
180
# returns the updated formats hash reference
181
#-----------------------------
182
sub editFormats {
183
   my ( $formats ) = @_;
184
   $formats = {} unless ref($formats) eq 'HASH';
185
   my $option = '';
186
   while ( $option ne 'Done' ) {
187
      my @formatNames = sort keys %$formats;
188
      print "\nCurrent formats:\n";
189
      if ( @formatNames ) {
190
         for my $name ( @formatNames ) {
191
            my $format = $formats->{$name};
192
            my $fname = ref($format) eq 'HASH' ? ($format->{'filename'} || '(no filename)') : '(invalid)';
193
            my $addStr = ref($format) eq 'HASH' ? ($format->{'additionalStrings'} || '') : '';
194
            print "  $name: $fname" . ($addStr ? " [strings: $addStr]" : "") . "\n";
195
         }
196
      } else {
197
         print "  (no formats defined)\n";
198
      }
199
      $option = &menuSelect( 
200
         "Manage formats", 
201
         [ @formatNames, 'Add new format', 'Done' ]
202
      );
203
      if ( $option eq 'Add new format' ) {
204
         print "Enter format name: ";
205
         my $formatName = <STDIN>;
206
         chomp $formatName;
207
         $formatName = trim($formatName);
208
         if ( $formatName ne '' ) {
209
            $formats->{$formatName} = {} unless exists $formats->{$formatName};
210
            print "Format '$formatName' created\n";
211
         }
212
      } elsif ( $option ne 'Done' ) {
213
         # Edit existing format
214
         my $formatName = $option;
215
         my $format = $formats->{$formatName};
216
         $format = {} unless ref($format) eq 'HASH';
217
         my $action = '';
218
         while ( $action ne 'Back' ) {
219
            print "\nFormat '$formatName':\n";
220
            print "  filename: " . ($format->{'filename'} || '(not set)') . "\n";
221
            print "  additionalStrings: " . ($format->{'additionalStrings'} || '(not set)') . "\n";
222
            $action = &menuSelect( "Edit format '$formatName'", [ 'Change filename', 'Change additionalStrings', 'Delete format', 'Back' ] );
223
            if ( $action eq 'Change filename' ) {
224
               print "Enter new filename (ROUTER and USER are replaced with actual values): ";
225
               my $filename = <STDIN>;
226
               chomp $filename;
227
               $format->{'filename'} = trim($filename);
228
            } elsif ( $action eq 'Change additionalStrings' ) {
229
               print "Enter new additionalStrings (\\n separated): ";
230
               my $addStr = <STDIN>;
231
               chomp $addStr;
232
               $format->{'additionalStrings'} = trim($addStr);
233
            } elsif ( $action eq 'Delete format' ) {
234
               my $confirm = &menuSelect( "Are you sure you want to delete format '$formatName'?", [ 'No', 'Yes' ] );
235
               if ( $confirm eq 'Yes' ) {
236
                  delete $formats->{$formatName};
237
                  print "Format '$formatName' deleted\n";
238
                  last;
239
               }
240
            }
241
         }
242
         $formats->{$formatName} = $format unless $action eq 'Delete format';
243
      }
244
   }
245
   return $formats;
246
}
247
 
248
#-----------------------------
16 rodolico 249
# editRouter: Edit the configuration for a router
250
# config - reference to the router's configuration hash
251
# routerName - name of the router being edited
252
# keys - reference to an array of keys (fields) that can be edited
253
# works on one router at a time, allows setting fields for that router
254
# returns true if configuration was saved, false if not
255
#-----------------------------
14 rodolico 256
sub editRouter {
16 rodolico 257
   my ( $config, $routerName, $keys ) = @_;
14 rodolico 258
   my $option = '';
16 rodolico 259
   my $saved = 1;
260
   my $currentRouter = $config->{$routerName};
261
   $currentRouter->{'template'} = 'PlainOpenVPN' unless defined $currentRouter->{'template'};
14 rodolico 262
   my $vpnProviders = ();
263
   while ( $option ne 'Done' ) {
16 rodolico 264
      print "\nEditing router configuration for $routerName" . ($saved ? "" : " (Changed)") . ":\n";
14 rodolico 265
      $option = &menuSelect( 
266
         "Select field to edit", 
16 rodolico 267
         [ @$keys, 'Save Config', 'Done' ], 
268
         [ map { defined $currentRouter->{$_} ? " (current: $currentRouter->{$_})" : " (not set)" } @$keys, '' ] 
14 rodolico 269
         );
16 rodolico 270
      if ( $option eq 'ovpnIndex' ) {
271
         $currentRouter->{'ovpnIndex'} = &selectOvpnIndex( $currentRouter );
272
         $saved = 0; # mark as unsaved
20 rodolico 273
      } elsif ( $option eq 'formats' ) {
274
         $currentRouter->{'formats'} = &editFormats( $currentRouter->{'formats'} );
275
         $saved = 0; # mark as unsaved
16 rodolico 276
      } elsif ( $option eq 'Save Config' ) {
277
         writeConfig( $configFileName, $config );
278
         $saved = 1; # mark as saved
279
         print "Configuration saved to $configFileName\n";
280
      } elsif ($option eq 'Done') {
281
         $option = &menuSelect( "Exiting configuration editor, but changes were not saved.", [ 'Exit', 'Save and Exit' ] );
282
         if ( $option eq 'Save and Exit' ) {
283
            writeConfig( $configFileName, $config );
284
            $saved = 1; # mark as saved
285
            print "Configuration saved to $configFileName\n";
14 rodolico 286
         }
16 rodolico 287
         last;
288
      } else {
289
         $saved = 0; # mark as unsaved
290
         print "Enter value for $option: ";
291
         my $value = <STDIN>;
292
         chomp $value;
293
         $value = trim($value);
294
         $currentRouter->{$option} = $value if $value ne '';
14 rodolico 295
      }
296
   }
16 rodolico 297
   return $saved;
14 rodolico 298
}
299
 
16 rodolico 300
$configFileName = shift if @ARGV;
301
# read the configuration file, if it exists, and place hash in $config
14 rodolico 302
my $config = &readConfig( $configFileName );
16 rodolico 303
# display menu of existing routers plus option to add new router
14 rodolico 304
my $routerName = &menuSelect( "Select router to configure", [ keys %$config, 'Add new router' ] );
16 rodolico 305
# adding a new router, so get the name, then create an empty hashref for it
14 rodolico 306
if ( $routerName eq 'Add new router' ) {
307
   print "Enter new router name: ";
308
   $routerName = <STDIN>;
309
   chomp $routerName;
310
   $routerName = trim($routerName);
311
   $config->{$routerName} = {};
312
}
16 rodolico 313
# edit the router configuration
314
&editRouter( $config, $routerName, \@fields );
14 rodolico 315
 
316
 
16 rodolico 317
1;