#!/usr/local/sbin/perl-suid -Tw /home/httpd/htdocs/proxysrc.cgi
# proxysrc.cgi -- Retrieves the origin of an OpenVPN proxy connection. 
# Copyright (c) 2020 by James F. Carter.  2020-11-27, Perl-5.32.0

# For evading criminal stupidity and net censorship, I run OpenVPN on Surya
# port 443/tcp.  It has this configuration command: 
#   port_share claude.cft.ca.us 443 /var/lib/wwwrun/openvpn-ps
# The directory (optional) is pre-created with owner wwwrun:nogroup mode 700.  
# When it detects a non-OpenVPN protocol (HTTPS), it does the reverse proxy 
# thing to the host and port specified, and it creates in the dir a file 
# telling who the upstream client is.  The file is removed when the connection
# is closed.  (To do this, OpenVPN has to run as root and not be in a chroot
# jail.)  For example, suppose:
#   Remote client 172.16.0.1 or fd18:dead:beef::c8 port 34567 connects 
#	to OpenVPN.  
#   It's proxied to claude from 192.168.0.185 or 2600:3c01::e6 
#	port 65432
#   OpenVPN creates filename "[AF_INET]192.168.0.185:65432"
#	or "[AF_INET6]2600:3c01::e6:65432" (last colon separated unit is
#	the port in decimal).
#   Its content is "[AF_INET]172.16.0.1:34567" or 
#	"[AF_INET6]fd18:dead:beef::c8:34567"

# Nasty quirk: This script is executed by wwwrun and the dir and files are
# owned by openvpn.  Recently (around 2020-07-xx I think) in OpenSuSE
# Tumbleweed, filesystem namespaces for non-root users are segregated.  The
# symptom here is that the target file cannot be opened by name, and when the
# directory is read, only the test file created by root is returned.  This
# script has to be setUID root; then it will open or enumerate the target file.

# An interested script on Claude, specifically whatismyip.cgi, should look in
# environment variables REMOTE_ADDR (should be Surya's) and REMOTE_PORT, and
# should put together a query to
# http://surya.cft.ca.us:80/proxysrc.cgi?192.168.0.185:65432 (IP colon port).
# This program will give a 1-line reply (text/plain) containing
# [AF_INET]172.16.0.1:34567 (the original client's IP and port) followed by
# debug information.  Append ';debug' to get the debug info.  If the requested
# connection info is not available, the first line will be empty (just a
# newline).

# Test command line: On Surya, 
#   echo "[AF_INET]192.168.1.2:65000" > \
#	"/var/lib/wwwrun/openvpn-ps/[AF_INET]192.9.200.185:33000"
# Or [AF_INET6]; IPv6 addresses are not bracketed.  Then execute
#   QUERY_STRING="[AF_INET]192.9.200.185:33000;debug" proxysrc.cgi
# And/or test from another host with
#   curl "http://surya/proxysrc.cgi?[AF_INET]192.9.200.185:33000;debug"
# Remember to delete the test file afterward.  

use IO::File;			# Also exports O_xxx from Fcntl

# Environment cleanup for setUID operation.  The path is 
# _PATH_STDPATH in /usr/include/paths.h .
delete @ENV{qw(IFS CDPATH ENV BASH_ENV LD_LIBRARY_PATH LD_RUN_PATH)};
$ENV{PATH} = "/usr/bin:/bin:/usr/sbin:/sbin";

# These options are not really settable from the command line.  
# Directory where OpenVPN puts its connection info files.  
our $opt_d = "/var/lib/wwwrun/openvpn-ps";
chdir $opt_d or do {
    print "\nCan't chdir $opt_d: $!\n";
    exit 0;
};

		# Sanitize the query string: just IP adrs, ports, and "debug".
($ENV{QUERY_STRING} // '') =~ /([\w.:;]*)/;
my %qs; @qs{qw(conn debug)} = (split(';', ($1 // '')), qw(missing1 missing2));

my $result = "\n";			# This is returned if no conn info.
my $i = rindex($qs{conn}, ':');
my $fname = ((index($qs{conn}, '.') > 0) ? '[AF_INET]' : '[AF_INET6]')
	. $qs{conn};
my $FH = IO::File->new($fname);
if ($FH) {
    $result = <$FH>;
    $result .= "\n" unless $result =~ /\n$/s;
}

if ($qs{debug} eq 'debug') {
    my %filemap;			# Key = filename, value = 1st/only line
    $filemap{extra} = "target filename = $fname";
    for $fname (glob("*")) {	# Bypass files not starting with family
	$FH = IO::File->new($fname) or next;
	chomp($filemap{$fname} = <$FH>);
    }
    $result .= join("", map {"$_\t= $filemap{$_}\n"} sort keys %filemap);
}

print "Content-type: text/plain; charset=utf-8\n\n$result";
exit 0;
