Subversion Repositories web_pages

Rev

Rev 16 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | Download | RSS feed

#! /usr/bin/env perl
 
 
use strict;
use warnings;

use lib '.'; # our libraries are in the current directory

use JSON; # for encode_json, decode_json
use Data::Dumper; # for debugging
use opnsense; # our module to handle opnsense API calls
use GD::Barcode::QRcode; # for creating the QR code images
use MIME::Base64 qw( decode_base64 ); # for decoding the ovpn file returned from the API
use File::Path qw( make_path ); # for creating directories
   
# Configuration file path
my $config_file = 'config.json';
# Users file path. This is shared between loadOpnSense.pl and index.php
my $users_file = 'users.json';
# Constants for creation of QR image
# this is used in URL created
my $issuer = "OPNsense";
# The size of individual "pixels" in graphics
my $moduleSize = 10;
# location to place the QR images
my $qrLocation = './qrcodes'; # cwd/qrcodes
# location to place the ovpn files
my $ovpnFileLocation = './openvpn_configs'; # cwd/openvpn
# following strings will be added to ovpn files if they don't exist
my $additionalOvpnStrings = "static-challenge 'Enter Auth Code' 0\nauth-nocache\n";

# load the configuration for the specified router from the config file
# returns a hash ref of the configuration for the specified entry (router name)
# if file does not exist or entry not found, returns empty hash ref
# file - name of file to read (default config.json if not specified)
# entry - name of the router to load configuration for
sub loadConfig {
    my ($file, $entry) = @_;
    if ( -e $file ) {
        open my $fh, '<', $file or die "Could not open '$file' $!";
        local $/;
        my $json_text = <$fh>;
        close $fh;
         my $config = decode_json($json_text);
         return $config->{$entry} if (exists $config->{$entry});
    }
    return {};
}

# load the users data structure from the specified file
sub loadUsers {
   my ( $file ) = @_;
   my $data = {};
   if ( -e $file ) {
      open( my $fh, '<', $file ) or die "Cannot open $file: $!";
      local $/;  # slurp mode
      my $json_text = <$fh>;
      close $fh;
      $data = decode_json($json_text);
   }
   return $data;
}

# save the users data structure to the specified file
sub saveUsers {
   my ( $file, $data ) = @_;
   open( my $fh, '>', $file ) or die "Cannot open $file: $!";
   print $fh encode_json($data);
   close $fh;
}

#
# makeQR
#
# Creates a QR file with a name of ($routername_)$account.png
# where $routername is prepended if it is set and $account is the user name
# an underscore separates the routername and the account (if routername is set)
# the contents of the qr are made up of the constants 'otpauth://totp/' followed by $issuer
# and the parameters secret (otp code) and issuer again.
sub makeQR {
   my ( $routerName, $account, $secret, $issuer, $moduleSize, $qrLocation ) = @_;
   my $otp_url = "otpauth://totp/$issuer:$account?secret=$secret&issuer=$issuer";
   # Ecc (Error Correction Code) can be one of Low, Medium, Quartile, and High
   # ModuleSize increases the actual size of each "pixel", enlarging for easier reading
   my $barcode = GD::Barcode::QRcode->new($otp_url, { Ecc => 'M', ModuleSize => $moduleSize } );
   my $image = $barcode->plot();
   # Save the image to a file. If there is a router name, prepend it to the filename with an underscore
   # allows the same user to be on two different routers
   my $outputFile = $qrLocation . '/' . ($routerName ? $routerName . '_' : '' ) . $account . '.png';
   open my $out, '>', "$outputFile" or die "Cannot write file $outputFile: $!";
   binmode $out;
   print $out $image->png;
   close $out;
   return $outputFile;
}

#
# makeVPNConfigFile
#
# Creates a configuration file for the user based on the template provided
# The template is read in and the following substitutions are made:
sub makeVPNConfigFile {
   my ( $contents, $routername, $username, $ovpnLocation, $additionalOvpnStrings ) = @_;
   # if contents is empty or does not have a content key, return empty string
   return '' unless $contents && $contents->{'content'};
   my $outputFileName = $ovpnLocation . '/' . ($routername ? $routername . '_' : '' ) . $username . '.ovpn';
   $contents->{'content'} = decode_base64($contents->{'content'});
   $contents->{'content'} .= "\n";
   $contents->{'content'} =~ s/(\r?\n)+/\n/g; # normalize line endings
   # add any additional strings if they are not already present
   my $comment = '';
   if ( $contents->{'content'} !~ /# Added by loadOpnSense.pl/ ) {
      $comment = "# Added by loadOpnSense.pl\n";
   }
   foreach my $string ( split( /\n/, $additionalOvpnStrings ) ) {
      unless ( $contents->{'content'} =~ /\$string/ ) {
         $contents->{'content'} .= "$comment$string\n";
         $comment = ''; # only add comment once
      }
   }
   open my $out, '>', "$outputFileName" or die "Cannot write file $outputFileName: $!";
   print $out $contents->{'content'};
   close $out;
   return $outputFileName;
}


### Main program

# get the router name from the command line
my $router = shift @ARGV or die "Usage: $0 <router_name>\n";
# Load existing configuration or initialize a new one
my $config = &loadConfig($config_file, $router );
die "Configuration for router $router not found in $config_file\n" unless $config && ref($config) eq 'HASH';

# load the users file. We will update the entry for this router
my $users = &loadUsers( $users_file );

# if there is no entry for this router, create an empty one
$users->{$router} = {} unless exists $users->{$router};
$users->{$router}->{'qrLocation'} = $qrLocation unless exists $users->{$router}->{'qrLocation'};
$users->{$router}->{'ovpnLocation'} = $ovpnFileLocation unless exists $users->{$router}->{'ovpnLocation'};
$users->{$router}->{'users'} = {}; # clean out the users list, we will reload it

# ensure the directories exist and give them full access
make_path( $users->{$router}->{'qrLocation'}, 0777 ) unless -d $users->{$router}->{'qrLocation'};
make_path( $users->{$router}->{'ovpnLocation'}, 0777 ) unless -d $users->{$router}->{'ovpnLocation'};

# this does most of the work, all the API calls are handled in the module
# create the opnsense object
my $opnsense = new opnsense(
   url    => $config->{'url'},
   apiKey    => $config->{'apiKey'},
   apiSecret => $config->{'apiSecret'},
   ovpnIndex  => $config->{'ovpnIndex'},
   localport => $config->{'localport'},
   template  => $config->{'template'},
   hostname  => $config->{'hostname'}, 
);

# get the VPN users. This is a hashref keyed by cert name, value is username
my $vpnCerts = $opnsense->get_vpn_users();

# convert the cert-"user" to username->certs, array ref of certs as not sure if multiple certs per user allowed
foreach my $cert ( keys %$vpnCerts ) {
   push @{$users->{$router}->{'users'}->{$vpnCerts->{$cert}}->{'certs'}}, $cert;
}

# these are all of the users on the system
my $allUsers = $opnsense->get_all_users();

# for each user in the system, if they are in the vpnUsers list, copy otp_seed, cert, and password
# we'll also delete any users who are disabled
my @keys = ('otp_seed', 'cert', 'password' ); # these are the keys we want to copy
foreach my $user ( keys %$allUsers ) {
   next unless exists $users->{$router}->{'users'}->{$user}; # skip users who do not have vpn certs
   if ( $allUsers->{$user}->{'disabled'} ) { # skip users who are disabled
      delete $users->{$router}->{'users'}->{$user};
      next;
   }
   foreach my $key ( @keys ) {
      $users->{$router}->{'users'}->{$user}->{$key} = ref($allUsers->{$user}->{$key}) ? '' : $allUsers->{$user}->{$key};
   }
}

# create the QR code file and VPN configuration for each user
foreach my $entry ( keys %{$users->{$router}->{'users'}} ) {
   my $account = $entry;
   my $secret = $users->{$router}->{'users'}->{$entry}->{'otp_seed'};
   $users->{$router}->{'users'}->{$entry}->{'qrFile'} = 
      &makeQR($router, $account, $secret, $issuer, $moduleSize, $users->{$router}->{'qrLocation'});
   if ( scalar(@{$users->{$router}->{'users'}->{$entry}->{'certs'}}) > 1 ) {
      warn "$entry has multiple certs, using first one only\n";
   }
   my $cert = $opnsense->get_vpn_config( $users->{$router}->{'users'}->{$entry}->{'certs'}->[0] );
   if ( !$cert || ref($cert) ne 'HASH' || !exists $cert->{'content'} ) {
      warn "Could not get VPN configuration for $entry, cert $users->{$router}->{'users'}->{$entry}->{'certs'}->[0]\n";
      next;
   }
   $users->{$router}->{'users'}->{$entry}->{'ovpnFile'} = 
      &makeVPNConfigFile( $cert, $router, $entry, $users->{$router}->{'ovpnLocation'}, $additionalOvpnStrings );
}

# save the users file
&saveUsers( $users_file, $users );