Subversion Repositories web_pages

Rev

Go to most recent revision | Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
14 rodolico 1
#! /usr/bin/env perl
2
 
3
 
4
use strict;
5
use warnings;
6
 
7
use lib '.'; # our libraries are in the current directory
8
 
9
use JSON; # for encode_json, decode_json
10
use Data::Dumper; # for debugging
11
use opnsense; # our module to handle opnsense API calls
12
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
14
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
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
 
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 {
38
    my ($file, $entry) = @_;
39
    if ( -e $file ) {
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);
45
         return $config->{$entry} if (exists $config->{$entry});
46
    }
47
    return {};
48
}
49
 
50
# load the users data structure from the specified file
51
sub loadUsers {
52
   my ( $file ) = @_;
53
   my $data = {};
54
   if ( -e $file ) {
55
      open( my $fh, '<', $file ) or die "Cannot open $file: $!";
56
      local $/;  # slurp mode
57
      my $json_text = <$fh>;
58
      close $fh;
59
      $data = decode_json($json_text);
60
   }
61
   return $data;
62
}
63
 
64
# save the users data structure to the specified file
65
sub saveUsers {
66
   my ( $file, $data ) = @_;
67
   open( my $fh, '>', $file ) or die "Cannot open $file: $!";
68
   print $fh encode_json($data);
69
   close $fh;
70
}
71
 
72
#
73
# makeQR
74
#
75
# Creates a QR file with a name of ($routername_)$account.png
76
# where $routername is prepended if it is set and $account is the user name
77
# an underscore separates the routername and the account (if routername is set)
78
# the contents of the qr are made up of the constants 'otpauth://totp/' followed by $issuer
79
# and the parameters secret (otp code) and issuer again.
80
sub makeQR {
81
   my ( $routerName, $account, $secret, $issuer, $moduleSize, $qrLocation ) = @_;
82
   my $otp_url = "otpauth://totp/$issuer:$account?secret=$secret&issuer=$issuer";
83
   # Ecc (Error Correction Code) can be one of Low, Medium, Quartile, and High
84
   # ModuleSize increases the actual size of each "pixel", enlarging for easier reading
85
   my $barcode = GD::Barcode::QRcode->new($otp_url, { Ecc => 'M', ModuleSize => $moduleSize } );
86
   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
88
   # allows the same user to be on two different routers
89
   my $outputFile = $qrLocation . '/' . ($routerName ? $routerName . '_' : '' ) . $account . '.png';
90
   open my $out, '>', "$outputFile" or die "Cannot write file $outputFile: $!";
91
   binmode $out;
92
   print $out $image->png;
93
   close $out;
94
   return $outputFile;
95
}
96
 
97
#
98
# makeVPNConfigFile
99
#
100
# Creates a configuration file for the user based on the template provided
101
# The template is read in and the following substitutions are made:
102
sub makeVPNConfigFile {
103
   my ( $contents, $routername, $username, $ovpnLocation, $additionalOvpnStrings ) = @_;
104
   # if contents is empty or does not have a content key, return empty string
105
   return '' unless $contents && $contents->{'content'};
106
   my $outputFileName = $ovpnLocation . '/' . ($routername ? $routername . '_' : '' ) . $username . '.ovpn';
107
   $contents->{'content'} = decode_base64($contents->{'content'});
108
   $contents->{'content'} .= "\n";
109
   $contents->{'content'} =~ s/(\r?\n)+/\n/g; # normalize line endings
110
   # add any additional strings if they are not already present
111
   my $comment = '';
112
   if ( $contents->{'content'} !~ /# Added by loadOpnSense.pl/ ) {
113
      $comment = "# Added by loadOpnSense.pl\n";
114
   }
115
   foreach my $string ( split( /\n/, $additionalOvpnStrings ) ) {
116
      unless ( $contents->{'content'} =~ /\$string/ ) {
117
         $contents->{'content'} .= "$comment$string\n";
118
         $comment = ''; # only add comment once
119
      }
120
   }
121
   open my $out, '>', "$outputFileName" or die "Cannot write file $outputFileName: $!";
122
   print $out $contents->{'content'};
123
   close $out;
124
   return $outputFileName;
125
}
126
 
127
 
128
### Main program
129
 
130
# get the router name from the command line
131
my $router = shift @ARGV or die "Usage: $0 <router_name>\n";
132
# Load existing configuration or initialize a new one
133
my $config = &loadConfig($config_file, $router );
134
die "Configuration for router $router not found in $config_file\n" unless $config && ref($config) eq 'HASH';
135
 
136
# load the users file. We will update the entry for this router
137
my $users = &loadUsers( $users_file );
138
 
139
# if there is no entry for this router, create an empty one
140
$users->{$router} = {} unless exists $users->{$router};
141
$users->{$router}->{'qrLocation'} = $qrLocation unless exists $users->{$router}->{'qrLocation'};
142
$users->{$router}->{'ovpnLocation'} = $ovpnFileLocation unless exists $users->{$router}->{'ovpnLocation'};
143
$users->{$router}->{'users'} = {}; # clean out the users list, we will reload it
144
 
145
# ensure the directories exist and give them full access
146
make_path( $users->{$router}->{'qrLocation'}, 0777 ) unless -d $users->{$router}->{'qrLocation'};
147
make_path( $users->{$router}->{'ovpnLocation'}, 0777 ) unless -d $users->{$router}->{'ovpnLocation'};
148
 
149
# this does most of the work, all the API calls are handled in the module
150
# create the opnsense object
151
my $opnsense = new opnsense(
152
   url    => $config->{'url'},
153
   apiKey    => $config->{'apiKey'},
154
   apiSecret => $config->{'apiSecret'},
155
   ovpnIndex  => $config->{'ovpnIndex'},
156
   localport => $config->{'localport'},
157
   template  => $config->{'template'},
158
   hostname  => $config->{'hostname'}, 
159
);
160
 
161
# get the VPN users. This is a hashref keyed by cert name, value is username
162
my $vpnCerts = $opnsense->get_vpn_users();
163
 
164
# 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 ) {
166
   push @{$users->{$router}->{'users'}->{$vpnCerts->{$cert}}->{'certs'}}, $cert;
167
}
168
 
169
# these are all of the users on the system
170
my $allUsers = $opnsense->get_all_users();
171
 
172
# 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
174
my @keys = ('otp_seed', 'cert', 'password' ); # these are the keys we want to copy
175
foreach my $user ( keys %$allUsers ) {
176
   next unless exists $users->{$router}->{'users'}->{$user}; # skip users who do not have vpn certs
177
   if ( $allUsers->{$user}->{'disabled'} ) { # skip users who are disabled
178
      delete $users->{$router}->{'users'}->{$user};
179
      next;
180
   }
181
   foreach my $key ( @keys ) {
182
      $users->{$router}->{'users'}->{$user}->{$key} = ref($allUsers->{$user}->{$key}) ? '' : $allUsers->{$user}->{$key};
183
   }
184
}
185
 
186
# create the QR code file and VPN configuration for each user
187
foreach my $entry ( keys %{$users->{$router}->{'users'}} ) {
188
   my $account = $entry;
189
   my $secret = $users->{$router}->{'users'}->{$entry}->{'otp_seed'};
190
   $users->{$router}->{'users'}->{$entry}->{'qrFile'} = 
191
      &makeQR($router, $account, $secret, $issuer, $moduleSize, $users->{$router}->{'qrLocation'});
192
   if ( scalar(@{$users->{$router}->{'users'}->{$entry}->{'certs'}}) > 1 ) {
193
      warn "$entry has multiple certs, using first one only\n";
194
   }
195
   my $cert = $opnsense->get_vpn_config( $users->{$router}->{'users'}->{$entry}->{'certs'}->[0] );
196
   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";
198
      next;
199
   }
200
   $users->{$router}->{'users'}->{$entry}->{'ovpnFile'} = 
201
      &makeVPNConfigFile( $cert, $router, $entry, $users->{$router}->{'ovpnLocation'}, $additionalOvpnStrings );
202
}
203
 
204
# save the users file
205
&saveUsers( $users_file, $users );