sap-vpn.pl
changeset 3 01314d620fe0
parent 2 5f08067bc677
child 5 9e64828e7d70
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sap-vpn.pl	Thu Nov 13 21:09:22 2008 +0000
@@ -0,0 +1,362 @@
+#! /usr/bin/perl -T
+# $Id$
+# $URL$
+# based on a version Uwe Werler downloaded from f5networks developer
+# area http://devcentral.f5.com/SDK/sslvpn.public.pl.txt
+
+use 5.8.8;
+use strict;
+use warnings;
+use Getopt::Long qw(:config no_ignore_case bundling);
+use Pod::Usage;
+use File::Basename;
+use English qw(-no_match_vars);
+
+($EUID, $UID) = ($UID, $EUID);    # release ROOT, doesn't harm, if not suid
+($0) = ($0 =~ /([\w-]+)/);        # untaint $0
+
+use constant ME => basename $0;
+
+delete @ENV{ grep /PATH/, keys %ENV };
+$ENV{PATH} = "/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/sbin:/bin";
+
+###
+### STEP 0 :: Verify that we have the necessary requirements.
+###
+### This script requires recent versions of Perl, OpenSSL and PPPD (including
+### the 'chat' program), and that they all be in our PATH.  This script was
+### written and tested with the following versions:
+###	FirePass:	5.5 and 6.0
+###	Perl:		5.8.8
+###	OpenSSL:	0.9.8b
+###	PPPD:		2.4.4
+
+###
+### STEP 1 :: Set up variables with the proper information to log in.
+###
+
+# Default values for the FQDN or IP of the FirePass we wish to connect to, the
+# name of our Network Access favorite, and our username/pasword.  All of these
+# can be passed as arguments, if desired.
+
+my $opt_user;
+my $opt_passcode;
+my $opt_host     = 'connectwdf.sap.com';
+my $opt_name     = 'SAP Network Access';
+my $opt_linkname = basename $0;
+my $opt_help     = 0;
+my $opt_man      = 0;
+my @ppp_opts     = ();
+my $opt_debug    = 0;
+my $opt_verbose  = 0;
+my $opt_script;
+my $opt_ppp = 1;
+
+sub untaint($) {
+    my ($x) = ($_[0] =~ /([\/\w.-]+)/i);
+    return $_[0] = $_[0] eq $x ? $x : undef;
+}
+
+sub verbose(@) { print STDERR @_ if $opt_verbose }
+sub debug(@)   { warn @_         if $opt_debug }
+
+MAIN: {
+
+    GetOptions(
+        "u|user=s"     => \$opt_user,
+        "p|passcode=s" => \$opt_passcode,
+        "H|host=s"     => \$opt_host,
+        "n|favorite=s" => \$opt_name,
+        "d|debug"      => \$opt_debug,
+        "o|opts=s"     => \@ppp_opts,
+        "h|help"       => \$opt_help,
+        "m|man"        => \$opt_man,
+        "s|script=s"   => \$opt_script,
+        "l|linkname"   => \$opt_linkname,
+        "ppp!"         => \$opt_ppp,
+        "v|verbose"    => \$opt_verbose,
+    ) or pod2usage();
+
+    pod2usage(-verbose => 1, -exitval => 0) if $opt_help;
+    pod2usage(-verbose => 2, -exitval => 0) if $opt_man;
+    pod2usage(-verbose => 0, -exitval => 0) if not defined $opt_user;
+
+    $opt_verbose += $opt_debug;
+
+    untaint($opt_user)
+      or die ME . ": username \"$opt_user\" didn't pass verification\n";
+
+    die ME . ": need to run with root permissions!\n"
+      if not $EUID == 0 || $UID == 0;
+
+    if (not defined $opt_passcode) {
+        chomp(my $settings = qx{stty "-g"});
+        open(IN, "/dev/tty") or die ME . ": Can't open /dev/tty: $!\n";
+        print "Passcode for $opt_user: ";
+        system stty => "-echo";
+        chomp($opt_passcode = <IN>);
+        system stty => $settings;
+        print "\n";
+    }
+
+    pod2usage(-verbose => 0, -exitval => 0) if not defined $opt_passcode;
+
+    untaint($opt_passcode) or die ME . ": passcode didn't pass verification\n";
+    untaint($opt_linkname) or die ME . ": linkname didn't pass verification\n";
+    untaint($opt_script) or die ME . ": script name didn't pass verification\n";
+
+    -x $opt_script
+      or die ME . ": Script $opt_script is not executable: $!\n"
+      if defined $opt_script;
+
+    push @ppp_opts, $opt_debug ? qw(nodetach debug dump) : qw(nolog updetach);
+    push @ppp_opts, "nodefaultroute" if not grep /^defaultroute$/, @ppp_opts;
+    push @ppp_opts, "logfd", 2 if $opt_verbose;
+    push @ppp_opts, "dryrun" if not $opt_ppp;
+    verbose "% PPP options: @ppp_opts\n";
+
+    # Declare variables used throughout the rest of the script.
+    my ($request, $response, $sessionid, $favorite);
+
+    # Store the OpenSSL command in a variable for convienence.
+    my $openssl =
+        "openssl s_client"
+      . (-d "/etc/ssl/certs" ? " -CApath /etc/ssl/certs" : "")
+      . " -ign_eof -quiet -connect ${opt_host}:443";
+
+    # Make initial request to get client_data
+    verbose "% initial request\n";
+    $request =
+        "GET /my.logon.php3?check=1 HTTP/1.0\r\n"
+      . "Content-Type: application/x-www-form-urlencoded\r\n"
+      . "Connection: close\r\n" . "\r\n";
+
+    $response = qx(echo "${request}" | ${openssl} 2>/dev/null)
+      or die ME . ": Invalid Host specified or not reachable: $opt_host\n";
+
+    $response =~ s/<INPUT type="hidden" name="client_data" value="(.*)">/$1/;
+    my $client_data = $1;
+
+###
+### STEP 2 :: Log in to FirePass.
+###
+
+    verbose "% login into firepass\n";
+
+  # This is the bare minimum required in order to successfully log in.  A normal
+  # browser will make many more requests than this to complete the log in
+  # sequence, but all that is required is this POST with our credentails.  This
+  # may fail if the FirePass has End-Point Security Policies configured.
+    $request =
+"check=1&username=${opt_user}&password=${opt_passcode}&mrhlogonform=1&client_data=${client_data}";
+
+    $request =
+        "POST /my.activation.php3 HTTP/1.0\r\n"
+      . "Host: ${opt_host}\r\n"
+      . "Content-Type: application/x-www-form-urlencoded\r\n"
+      . "Content-Length: "
+      . length($request) . "\r\n"
+      . "Connection: close\r\n" . "\r\n"
+      . "${request}\r\n";
+
+    $response = qx(echo "${request}" | ${openssl} 2>/dev/null);
+
+  # We can then parse the response for the MRHSession Cookie, which contains our
+  # SessionID.  In this example, we print out the SessionID in order to verify
+  # that our log in attempt worked.
+    $response =~ /MRHSession=(\w+);/;
+    $sessionid = $1;
+    verbose "% session id ${sessionid}\n";
+
+###
+### STEP 3 :: Create the SSL VPN tunnel.
+###
+
+    verbose "% creating the ssl tunnel\n";
+
+   # Now that we are authenticated and have a valid SessionID, we must request
+   # specific pages/objects in order to initiate a SSL VPN tunnel.  Before we do
+   # this, let's determine the resource locator for our Network Access favorite.
+    $request = "GET /vdesk/vpn/index.php3?outform=xml HTTP/1.0\r\n"
+      . "Cookie: MRHSession=${sessionid}\r\n" . "\r\n";
+    $response = qx(echo "${request}" | ${openssl} 2>/dev/null);
+
+ # The response is XML, so we can safely grab what we are looking for using some
+ # regular expression magic.  Same with the SessionID, we're printing out the
+ # final value to make sure we're on the right track.
+    $response =~ /${opt_name}[^\n]+\n[^Z]+Z=\d+,(\d+)/;
+    $favorite = $1;
+    verbose "% favorite ${favorite}\n";
+
+    # We're all set!  Let's visit the necessary pages/objects to notify FirePass
+    # that we wish to open a SSL VPN tunnel.
+    foreach my $uri ("/vdesk/", "/vdesk/vpn/connect.php3?Z=0,${favorite}",) {
+        $request = "GET ${uri} HTTP/1.0\r\n"
+          . "Cookie: MRHSession=${sessionid}\r\n" . "\r\n";
+        system("echo \"${request}\" | ${openssl} >/dev/null 2>&1");
+    }
+
+# We are now authenticated, and have requested the necessary pre-tunnel
+# pages/objects.  It's time to start our SSL VPN connection.  To do this, we are
+# simply calling PPPD, and having it use OpenSSL as a psuedo terminal device.
+    $request = "GET /myvpn?sess=${sessionid} HTTP/1.0\r\n"
+      . "Cookie: MRHSession=${sessionid}\r\n" . "\r\n";
+    $request = "chat -v '' '${request}'";
+
+    {
+        my $pid = fork();    # this way we've better control
+        die ME . ": can't fork: $!\n" if not defined $pid;
+
+        if ($pid == 0) {
+            $EUID = $UID;    # get ROOT, the pppd want's to see
+                             # the UID and the EUID to be ROOT
+            exec(
+                pppd     => @ppp_opts,
+                pty      => ${openssl},
+                connect  => ${request},
+                linkname => $opt_linkname,
+                qw(noauth crtscts passive noproxyarp local)
+            ) or die ME . ": Can't exec: $!\n";
+
+        }
+        wait;
+	die ME . ": pppd didn't return sucessfully (rc: @{[$? >> 8]})\n" if $?;
+    }
+
+    # now executing the script
+    # if there is some script name supplied
+
+    if (defined $opt_script and not $opt_debug) {
+
+        my $pid = fork();
+        die ME . ": Can't fork: $!\n" if not defined $pid;
+
+        if ($pid == 0) {    # child
+
+            if ($UID != $EUID) {
+                my ($owner, $group) = (stat $opt_script)[ 4, 5 ];
+                die ME . ": can't stat $opt_script\n" if not defined $owner;
+
+                $UID = $EUID = $owner;
+                $GID = $EGID = $group;
+            }
+
+            verbose "% executing script $opt_script with uid $UID and gid $GID\n";
+
+            exec $opt_script
+              or die ME . ": Can't exec $opt_script: $!\n";
+        }
+        $pid = wait;
+        verbose "% script returned $?\n";
+        exit;
+    }
+
+    # Voila!  We should now have a PPP connection running over SSL.  We can exit
+    # from this script cleanly, and move on to setting up routes to the remote
+    # network using our favorite networking tools.  Happy hacking!
+
+    exit(0);
+
+}
+
+__END__
+
+=head1 SYNOPSIS
+
+ sap-vpn [-d|--debug] [-v|--verbose] [--[no]ppp] 
+     [-o|--opt <ppp option>]
+     [-u|--user <user>] [-p|--passcode <passcode>]
+     [-H|--host <host>]
+     [-s|--script <script>]
+
+ sap-vpn [-h|--help]
+ sap-vpn [-m|--man]
+
+=head1 DESCRIPTION
+
+Ths script establishes a VPN connecttion to a FirePass server.
+B<Note:> This script requires B<root> privileges and should be run via sudo
+or some other approbiate mechanism. (You may install it SUID root. This
+is the safest way, since the script rises its privileges only when
+necessary, instead of operating all the time as root.)
+
+=head1 OPTIONS
+
+=over
+
+=item B<-d>
+
+Print debug output from pppd. In this mode the script (and the pppd)
+will not detach from your terminal and the script code will not be
+executed.  (default: 0)
+
+=item B<-h>|B<--help>
+
+A short help. (defualt: off)
+
+=item B<-H>|B<--host> I<host>
+
+Host to connect to. (defaults to C<connectwdf.sap.corp>)
+
+=item B<-l>|B<--linkname> I<linkname>
+
+The name for the PPP link. Only word characters and "-" (minus sign) are
+allowed. (default: base name of this script)
+
+=item B<-m>|B<--man>
+
+This man page. (default: off)
+
+=item B<-n>|B<--name> I<name>
+
+Name of the service(?). (default: to 'SAP Network Access')
+
+=item B<-o>|B<--opts> I<ppp options>
+
+Additional options for ppp. Can be specified multiple times. (default: nodetach)
+Good choices are C<detach>, or C<updetach>.
+B<Note:> The C<nodefaultroute> is set automatically.
+
+=item B<-p>|B<--passcode> I<passcode>
+
+Your passcode, if not supplied, F</dev/tty> is opened for reading
+the passcode. (no default)
+
+=item B<--[no]ppp>
+
+Start (don't start) the PPP daemon. This option is for debugging purpose
+only. (default: on)
+
+=item B<-s>|B<--script> I<script>
+
+Script to execute after establishing the IP link.  If we're running as
+SUID-root, this script is executed under the effective UID and GID of
+its owner, otherwise just with the permissions we have. (See the B<-d>
+option above!). (no default)
+
+    #! /bin/bash
+    # How to know the interface name and the local/remove
+    # ip addresses?
+    # Please se below.
+
+B<Note:> You should consider using the F</etc/ppp/ip-up.d/> scripts in
+favour of this option!  The of the "ip-up"-script is having access to
+all the environment variables set by the PPP daemon:
+
+    #! /bin/bash
+    # example /etc/ppp/ip-up.d/<SCRIPT>
+    test "$LINKNAME" = "sap-vpn" || exit 0
+    ip route add 10.21.0.0/16 dev $PPP_IFACE
+    ip route add 10.17.0.0/16 dev $PPP_IFACE
+
+=item B<-u>|B<--user> I<user>
+
+Your D|C|I user, not really optional. (no default)
+
+=back
+
+=cut
+
+###
+### End of file.
+###