| #!/usr/bin/perl |
| # |
| # Copyright (c) 2006-2010 by Karl J. Runge <[email protected]> |
| # |
| # connect_switch is free software; you can redistribute it and/or modify |
| # it under the terms of the GNU General Public License as published by |
| # the Free Software Foundation; either version 2 of the License, or (at |
| # your option) any later version. |
| # |
| # connect_switch is distributed in the hope that it will be useful, |
| # but WITHOUT ANY WARRANTY; without even the implied warranty of |
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| # GNU General Public License for more details. |
| # |
| # You should have received a copy of the GNU General Public License |
| # along with connect_switch; if not, write to the Free Software |
| # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA |
| # or see <http://www.gnu.org/licenses/>. |
| # |
| # |
| # connect_switch: |
| # |
| # A kludge script that sits between web clients and a mod_ssl (https) |
| # enabled apache webserver. |
| # |
| # If an incoming web client connection makes a proxy CONNECT request |
| # it is handled directly by this script (apache is not involved). |
| # Otherwise, all other connections are forwarded to the apache webserver. |
| # |
| # This can be useful for VNC redirection using an existing https (port |
| # 443) webserver, thereby not requiring a 2nd (non-https) port open on |
| # the firewall for the CONNECT requests. |
| # |
| # It does not seem possible (to me) to achieve this entirely within apache |
| # because the CONNECT request appears to be forwarded encrypted to |
| # the remote host and so the SSL dies immediately. |
| # |
| # It can also be used to redirect ANY protocol, e.g. SSH, not just VNC. |
| # See CONNECT_SWITCH_APPLY_VNC_OFFSET=0 to disable VNC 5900 shift. |
| # |
| # Note: There is no need to use this script for a non-ssl apache webserver |
| # port because mod_proxy works fine for doing the switching all inside |
| # apache (see ProxyRequests and AllowCONNECT parameters). |
| # |
| # |
| # Apache configuration: |
| # |
| # The mod_ssl configuration is often in a file named ssl.conf. In the |
| # simplest case you change something like this: |
| # |
| # From: |
| # |
| # Listen 443 |
| # |
| # <VirtualHost _default_:443> |
| # ... |
| # </VirtualHost> |
| # |
| # To: |
| # |
| # Listen 127.0.0.1:443 |
| # |
| # <VirtualHost _default_:443> |
| # ... |
| # </VirtualHost> |
| # |
| # (i.e. just change the Listen directive). |
| # |
| # If you have mod_ssl listening on a different internal port, you do |
| # not need to specify the localhost Listen address. |
| # |
| # It is probably a good idea to set $listen_host below to the known |
| # IP address you want the service to listen on (to avoid localhost where |
| # apache is listening). |
| # |
| |
| #################################################################### |
| # NOTE: For more info on configuration settings, read below for |
| # all of the CONNECT_SWITCH_* env. var. parameters. |
| #################################################################### |
| |
| |
| #################################################################### |
| # Allow env vars to also be specified on cmdline: |
| # |
| foreach my $arg (@ARGV) { |
| if ($arg =~ /^(CONNECT_SWITCH.*?)=(.*)$/) { |
| $ENV{$1} = $2; |
| } |
| } |
| |
| # Set up logging: |
| # |
| if (exists $ENV{CONNECT_SWITCH_LOGFILE}) { |
| close STDOUT; |
| if (!open(STDOUT, ">>$ENV{CONNECT_SWITCH_LOGFILE}")) { |
| die "connect_switch: $ENV{CONNECT_SWITCH_LOGFILE} $!\n"; |
| } |
| close STDERR; |
| open(STDERR, ">&STDOUT"); |
| } |
| select(STDERR); $| = 1; |
| select(STDOUT); $| = 1; |
| |
| # interrupt handler: |
| # |
| my $looppid = ''; |
| my $pidfile = ''; |
| my $listen_sock = ''; # declared here for get_out() |
| # |
| sub get_out { |
| print STDERR "$_[0]:\t$$ looppid=$looppid\n"; |
| close $listen_sock if $listen_sock; |
| if ($looppid) { |
| kill 'TERM', $looppid; |
| fsleep(0.2); |
| } |
| unlink $pidfile if $pidfile; |
| exit 0; |
| } |
| $SIG{INT} = \&get_out; |
| $SIG{TERM} = \&get_out; |
| |
| # pidfile: |
| # |
| sub open_pidfile { |
| if (exists $ENV{CONNECT_SWITCH_PIDFILE}) { |
| my $pf = $ENV{CONNECT_SWITCH_PIDFILE}; |
| if (open(PID, ">$pf")) { |
| print PID "$$\n"; |
| close PID; |
| $pidfile = $pf; |
| } else { |
| print STDERR "could not open pidfile: $pf - $! - continuing...\n"; |
| } |
| delete $ENV{CONNECT_SWITCH_PIDFILE}; |
| } |
| } |
| |
| #################################################################### |
| # Set CONNECT_SWITCH_LOOP=1 to have this script create an outer loop |
| # restarting itself if it ever exits. Set CONNECT_SWITCH_LOOP=BG to |
| # do this in the background as a daemon. |
| |
| if (exists $ENV{CONNECT_SWITCH_LOOP}) { |
| my $csl = $ENV{CONNECT_SWITCH_LOOP}; |
| if ($csl ne 'BG' && $csl ne '1') { |
| die "connect_switch: invalid CONNECT_SWITCH_LOOP.\n"; |
| } |
| if ($csl eq 'BG') { |
| # go into bg as "daemon": |
| setpgrp(0, 0); |
| my $pid = fork(); |
| if (! defined $pid) { |
| die "connect_switch: $!\n"; |
| } elsif ($pid) { |
| wait; |
| exit 0; |
| } |
| if (fork) { |
| exit 0; |
| } |
| setpgrp(0, 0); |
| close STDIN; |
| if (! $ENV{CONNECT_SWITCH_LOGFILE}) { |
| close STDOUT; |
| close STDERR; |
| } |
| } |
| delete $ENV{CONNECT_SWITCH_LOOP}; |
| |
| if (exists $ENV{CONNECT_SWITCH_PIDFILE}) { |
| open_pidfile(); |
| } |
| |
| print STDERR "connect_switch: starting service at ", scalar(localtime), " master-pid=$$\n"; |
| while (1) { |
| $looppid = fork; |
| if (! defined $looppid) { |
| sleep 10; |
| } elsif ($looppid) { |
| wait; |
| } else { |
| exec $0; |
| exit 1; |
| } |
| print STDERR "connect_switch: re-starting service at ", scalar(localtime), " master-pid=$$\n"; |
| sleep 1; |
| } |
| exit 0; |
| } |
| if (exists $ENV{CONNECT_SWITCH_PIDFILE}) { |
| open_pidfile(); |
| } |
| |
| |
| ############################################################################ |
| # The defaults for hosts and ports (you can override them below if needed): |
| # |
| # Look below for these environment variables that let you set the various |
| # parameters without needing to edit this script: |
| # |
| # CONNECT_SWITCH_LISTEN |
| # CONNECT_SWITCH_HTTPD |
| # CONNECT_SWITCH_ALLOWED |
| # CONNECT_SWITCH_ALLOW_FILE |
| # CONNECT_SWITCH_VERBOSE |
| # CONNECT_SWITCH_APPLY_VNC_OFFSET |
| # CONNECT_SWITCH_VNC_OFFSET |
| # CONNECT_SWITCH_LISTEN_IPV6 |
| # CONNECT_SWITCH_BUFSIZE |
| # CONNECT_SWITCH_LOGFILE |
| # CONNECT_SWITCH_PIDFILE |
| # CONNECT_SWITCH_MAX_CONNECTIONS |
| # |
| # You can also set these on the cmdline: |
| # connect_switch CONNECT_SWITCH_LISTEN=X CONNECT_SWITCH_ALLOW_FILE=Y ... |
| # |
| |
| # By default we will use hostname and assume it resolves: |
| # |
| my $hostname = `hostname`; |
| chomp $hostname; |
| |
| my $listen_host = $hostname; |
| my $listen_port = 443; |
| |
| # Let user override listening situation, e.g. multihomed: |
| # |
| if (exists $ENV{CONNECT_SWITCH_LISTEN}) { |
| # |
| # E.g. CONNECT_SWITCH_LISTEN=192.168.0.32:443 |
| # |
| $listen_host = ''; |
| $listen_port = ''; |
| if ($ENV{CONNECT_SWITCH_LISTEN} =~ /^(.*):(\d+)$/) { |
| ($listen_host, $listen_port) = ($1, $2); |
| } |
| } |
| |
| my $httpd_host = 'localhost'; |
| my $httpd_port = 443; |
| |
| if (exists $ENV{CONNECT_SWITCH_HTTPD}) { |
| # |
| # E.g. CONNECT_SWITCH_HTTPD=127.0.0.1:443 |
| # |
| $httpd_host = ''; |
| $httpd_port = ''; |
| if ($ENV{CONNECT_SWITCH_HTTPD} =~ /^(.*):(\d+)$/) { |
| ($httpd_host, $httpd_port) = ($1, $2); |
| } |
| } |
| |
| my $bufsize = 8192; |
| if (exists $ENV{CONNECT_SWITCH_BUFSIZE}) { |
| # |
| # E.g. CONNECT_SWITCH_BUFSIZE=32768 |
| # |
| $bufsize = $ENV{CONNECT_SWITCH_BUFSIZE}; |
| } |
| |
| |
| ############################################################################ |
| # You can/should override the host/port settings here: |
| # |
| #$listen_host = '23.45.67.89'; # set to your interface IP number. |
| #$listen_port = 555; # and/or nonstandard port. |
| #$httpd_host = 'somehost'; # maybe you redir https to another machine. |
| #$httpd_port = 666; # and/or nonstandard port. |
| |
| # You must set the allowed host:port CONNECT redirection list. |
| # Only these host:port pairs will be redirected to. |
| # Port ranges are allowed too: host:5900-5930. |
| # If there is one entry named ALL all connections are allow. |
| # You must supply something, default is deny. |
| # |
| my @allowed = qw( |
| machine1:5915 |
| machine2:5900 |
| ); |
| |
| if (exists $ENV{CONNECT_SWITCH_ALLOWED}) { |
| # |
| # E.g. CONNECT_SWITCH_ALLOWED=machine1:5915,machine2:5900 |
| # |
| @allowed = split(/,/, $ENV{CONNECT_SWITCH_ALLOWED}); |
| } |
| |
| # Or you could also use an external "allow file". |
| # They get added to the @allowed list. |
| # The file is re-read for each new connection. |
| # |
| # Format of $allow_file: |
| # |
| # host1 vncdisp |
| # host2 vncdisp |
| # |
| # where, e.g. vncdisp = 15 => port 5915, say |
| # |
| # joesbox 15 |
| # fredsbox 15 |
| # rupert 1 |
| |
| # For examply, mine is: |
| # |
| my $allow_file = '/dist/apache/2.0/conf/vnc.hosts'; |
| $allow_file = ''; |
| |
| if (exists $ENV{CONNECT_SWITCH_ALLOW_FILE}) { |
| # E.g. CONNECT_SWITCH_ALLOW_FILE=/usr/local/etc/allow.txt |
| $allow_file = $ENV{CONNECT_SWITCH_ALLOW_FILE}; |
| } |
| |
| # Set to 1 to re-map to vnc port, e.g. 'hostname 15' to 'hostname 5915' |
| # i.e. assume a port 0 <= port < 200 is actually a VNC display |
| # and add 5900 to it. Set to 0 to not do the mapping. |
| # Note that negative ports, e.g. 'joesbox -22' go directly to -port. |
| # |
| my $apply_vnc_offset = 1; |
| my $vnc_offset = 5900; |
| |
| if (exists $ENV{CONNECT_SWITCH_APPLY_VNC_OFFSET}) { |
| # |
| # E.g. CONNECT_SWITCH_APPLY_VNC_OFFSET=0 |
| # |
| $apply_vnc_offset = $ENV{CONNECT_SWITCH_APPLY_VNC_OFFSET}; |
| } |
| if (exists $ENV{CONNECT_SWITCH_VNC_OFFSET}) { |
| # |
| # E.g. CONNECT_SWITCH_VNC_OFFSET=6000 |
| # |
| $vnc_offset = $ENV{CONNECT_SWITCH_VNC_OFFSET}; |
| } |
| |
| # Set to 1 or higher for more info output: |
| # |
| my $verbose = 0; |
| |
| if (exists $ENV{CONNECT_SWITCH_VERBOSE}) { |
| # |
| # E.g. CONNECT_SWITCH_VERBOSE=1 |
| # |
| $verbose = $ENV{CONNECT_SWITCH_VERBOSE}; |
| } |
| |
| # zero means loop forever, positive value means exit after handling that |
| # many connections. |
| # |
| my $cmax = 0; |
| if (exists $ENV{CONNECT_SWITCH_MAX_CONNECTIONS}) { |
| $cmax = $ENV{CONNECT_SWITCH_MAX_CONNECTIONS}; |
| } |
| |
| |
| #=========================================================================== |
| # No need for any changes below here. |
| #=========================================================================== |
| |
| use IO::Socket::INET; |
| use strict; |
| use warnings; |
| |
| # Test for INET6 support: |
| # |
| my $have_inet6 = 0; |
| eval "use IO::Socket::INET6;"; |
| $have_inet6 = 1 if $@ eq ""; |
| |
| my $killpid = 1; |
| |
| setpgrp(0, 0); |
| |
| if (exists $ENV{CONNECT_SWITCH_LISTEN_IPV6}) { |
| # note we leave out LocalAddr. |
| my $cmd = ' |
| $listen_sock = IO::Socket::INET6->new( |
| Listen => 10, |
| LocalPort => $listen_port, |
| ReuseAddr => 1, |
| Domain => AF_INET6, |
| Proto => "tcp" |
| ); |
| '; |
| eval $cmd; |
| die "$@\n" if $@; |
| } else { |
| $listen_sock = IO::Socket::INET->new( |
| Listen => 10, |
| LocalAddr => $listen_host, |
| LocalPort => $listen_port, |
| ReuseAddr => 1, |
| Proto => "tcp" |
| ); |
| } |
| |
| if (! $listen_sock) { |
| die "connect_switch: $!\n"; |
| } |
| |
| my $current_fh1 = ''; |
| my $current_fh2 = ''; |
| |
| my $conn = 0; |
| |
| while (1) { |
| $conn++; |
| if ($cmax > 0 && $conn > $cmax) { |
| print STDERR "last connection ($cmax)\n" if $verbose; |
| last; |
| } |
| print STDERR "listening for connection: $conn\n" if $verbose; |
| my ($client, $ip) = $listen_sock->accept(); |
| if (! $client) { |
| fsleep(0.5); |
| next; |
| } |
| print STDERR "conn: $conn -- ", $client->peerhost(), " at ", scalar(localtime), "\n" if $verbose; |
| |
| my $pid = fork(); |
| if (! defined $pid) { |
| die "connect_switch: $!\n"; |
| } elsif ($pid) { |
| wait; |
| next; |
| } else { |
| close $listen_sock; |
| if (fork) { |
| exit 0; |
| } |
| setpgrp(0, 0); |
| handle_conn($client); |
| } |
| } |
| |
| exit 0; |
| |
| sub handle_conn { |
| my $client = shift; |
| |
| my $start = time(); |
| |
| my @allow = @allowed; |
| |
| # read allow file. Note we read it for every connection |
| # to allow the admin to modify it w/o restarting us. |
| # better way would be to read in parent and check mtime. |
| # |
| if ($allow_file && -f $allow_file) { |
| if (open(ALLOW, "<$allow_file")) { |
| while (<ALLOW>) { |
| next if /^\s*#/; |
| next if /^\s*$/; |
| chomp; |
| my ($host, $dpy) = split(' ', $_); |
| next if ! defined $host; |
| next if ! defined $dpy; |
| if ($dpy < 0) { |
| $dpy = -$dpy; |
| } elsif ($apply_vnc_offset) { |
| $dpy += $vnc_offset if $dpy < 200; |
| } |
| push @allow, "$host:$dpy"; |
| } |
| close(ALLOW); |
| } else { |
| warn "$allow_file: $!\n"; |
| } |
| } |
| |
| # Read the first 7 bytes of connection, see if it is 'CONNECT' |
| # |
| my $str = ''; |
| my $N = 0; |
| my $isconn = 1; |
| for (my $i = 0; $i < 7; $i++) { |
| my $b; |
| sysread($client, $b, 1); |
| $str .= $b; |
| $N++; |
| print STDERR "read: '$str'\n" if $verbose > 1; |
| my $cstr = substr('CONNECT', 0, $i+1); |
| if ($str ne $cstr) { |
| $isconn = 0; |
| last; |
| } |
| } |
| |
| my $sock = ''; |
| |
| if ($isconn) { |
| # it is CONNECT, read rest of HTTP header: |
| # |
| while ($str !~ /\r\n\r\n/) { |
| my $b; |
| sysread($client, $b, 1); |
| $str .= $b; |
| } |
| print STDERR "read: $str\n" if $verbose > 1; |
| |
| # get http version and host:port |
| # |
| my $ok = 0; |
| my $hostport = ''; |
| my $http_vers = '1.0'; |
| if ($str =~ /^CONNECT\s+(\S+)\s+HTTP\/(\S+)/) { |
| $hostport = $1; |
| $http_vers = $2; |
| my $h = ''; |
| my $p = ''; |
| |
| if ($hostport =~ /^(.*):(\d+)$/) { |
| ($h, $p) = ($1, $2); |
| } |
| if ($p =~ /^\d+$/) { |
| # check allowed host list: |
| foreach my $hp (@allow) { |
| if ($hp eq 'ALL') { |
| $ok = 1; |
| } |
| if ($hp eq $hostport) { |
| $ok = 1; |
| } |
| if ($hp =~ /^(.*):(\d+)-(\d+)$/) { |
| my $ahost = $1; |
| my $pmin = $2; |
| my $pmax = $3; |
| if ($h eq $ahost) { |
| if ($p >= $pmin && $p <= $pmax) { |
| $ok = 1; |
| } |
| } |
| } |
| last if $ok; |
| } |
| } |
| } |
| |
| my $msg_1 = "HTTP/$http_vers 200 Connection Established\r\n" |
| . "Proxy-agent: connect_switch v0.2\r\n\r\n"; |
| my $msg_2 = "HTTP/$http_vers 502 Bad Gateway\r\n" |
| . "Connection: close\r\n\r\n"; |
| |
| if (! $ok) { |
| # disallowed. drop with message. |
| # |
| syswrite($client, $msg_2, length($msg_2)); |
| close $client; |
| exit 0; |
| } |
| |
| my $host = ''; |
| my $port = ''; |
| |
| if ($hostport =~ /^(.*):(\d+)$/) { |
| ($host, $port) = ($1, $2); |
| } |
| |
| print STDERR "connecting to: $host:$port\n" if $verbose; |
| |
| $sock = IO::Socket::INET->new( |
| PeerAddr => $host, |
| PeerPort => $port, |
| Proto => "tcp" |
| ); |
| print STDERR "connect to host='$host' port='$port' failed: $!\n" if !$sock; |
| if (! $sock && $have_inet6) { |
| eval {$sock = IO::Socket::INET6->new( |
| PeerAddr => $host, |
| PeerPort => $port, |
| Proto => "tcp" |
| );}; |
| print STDERR "connect to host='$host' port='$port' failed: $! (ipv6)\n" if !$sock; |
| } |
| my $msg; |
| |
| # send the connect proxy reply: |
| # |
| if ($sock) { |
| $msg = $msg_1; |
| } else { |
| $msg = $msg_2; |
| } |
| syswrite($client, $msg, length($msg)); |
| $str = ''; |
| } else { |
| # otherwise, redirect to apache for normal https: |
| # |
| print STDERR "connecting to: $httpd_host:$httpd_port\n" if $verbose; |
| $sock = IO::Socket::INET->new( |
| PeerAddr => $httpd_host, |
| PeerPort => $httpd_port, |
| Proto => "tcp" |
| ); |
| if (! $sock && $have_inet6) { |
| eval {$sock = IO::Socket::INET6->new( |
| PeerAddr => $httpd_host, |
| PeerPort => $httpd_port, |
| Proto => "tcp" |
| );}; |
| } |
| } |
| |
| if (! $sock) { |
| close $client; |
| die "connect_switch: $!\n"; |
| } |
| |
| # get ready for xfer phase: |
| # |
| $current_fh1 = $client; |
| $current_fh2 = $sock; |
| |
| $SIG{TERM} = sub {print STDERR "got sigterm\[$$]\n" if $verbose; close $current_fh1; close $current_fh2; exit 0}; |
| |
| my $parent = $$; |
| if (my $child = fork()) { |
| xfer($sock, $client, 'S->C'); |
| if ($killpid) { |
| fsleep(0.5); |
| kill 'TERM', $child; |
| } |
| } else { |
| # write those first bytes if not CONNECT: |
| # |
| if ($str ne '' && $N > 0) { |
| syswrite($sock, $str, $N); |
| } |
| xfer($client, $sock, 'C->S'); |
| if ($killpid) { |
| fsleep(0.75); |
| kill 'TERM', $parent; |
| } |
| } |
| if ($verbose > 1) { |
| my $dt = time() - $start; |
| print STDERR "duration\[$$]: $dt seconds. ", scalar(localtime), "\n"; |
| } |
| exit 0; |
| } |
| |
| sub xfer { |
| my($in, $out, $lab) = @_; |
| my ($RIN, $WIN, $EIN, $ROUT); |
| $RIN = $WIN = $EIN = ""; |
| $ROUT = ""; |
| vec($RIN, fileno($in), 1) = 1; |
| vec($WIN, fileno($in), 1) = 1; |
| $EIN = $RIN | $WIN; |
| my $buf; |
| |
| while (1) { |
| my $nf = 0; |
| while (! $nf) { |
| $nf = select($ROUT=$RIN, undef, undef, undef); |
| } |
| my $len = sysread($in, $buf, $bufsize); |
| if (! defined($len)) { |
| next if $! =~ /^Interrupted/; |
| print STDERR "connect_switch\[$lab/$conn/$$]: $!\n"; |
| last; |
| } elsif ($len == 0) { |
| print STDERR "connect_switch\[$lab/$conn/$$]: " |
| . "Input is EOF.\n"; |
| last; |
| } |
| |
| if (0) { |
| # very verbose debugging of data: |
| syswrite(STDERR , "\n$lab: ", 6); |
| syswrite(STDERR , $buf, $len); |
| } |
| |
| my $offset = 0; |
| my $quit = 0; |
| while ($len) { |
| my $written = syswrite($out, $buf, $len, $offset); |
| if (! defined $written) { |
| print STDERR "connect_switch\[$lab/$conn/$$]: " |
| . "Output is EOF. $!\n"; |
| $quit = 1; |
| last; |
| } |
| $len -= $written; |
| $offset += $written; |
| } |
| last if $quit; |
| } |
| close($in); |
| close($out); |
| } |
| |
| sub fsleep { |
| my ($time) = @_; |
| select(undef, undef, undef, $time) if $time; |
| } |