/*
 * Copyright (c) 2000-2007, OpenFWTK Development Group
 * All rights reserved. See LICENSE.
 */

/*
 * ssl-gw, CONNECT method proxy for OpenFWTK
 *
 * functionality duplicates squid-gw's connect method but i decided to
 * keep it because it is really small and does _not_ handle GET requests
 * (which behaviour is somehow useful when we deal with misbehaving programs).
 * We may also enforce some check if actual ssl negotiation appears in
 * the tunnel (not implemented yet).
 *
 * Another useful feature is we can plug-to to existing CONNECT proxy
 * thus keeping ACLs and accouting on the firewall but using parent proxy for
 * outbound connections.
 *
 * ACL handling differs from other proxies: we may specify host:port
 * pairs instead of just destination hostnames (credits to 
 * Jean-Cristophe Touvet <jct@edelweb.fr>, some generic portions of code 
 * used here)
 *
 * (C) Copyright 2000..2002 ArkanoiD <ark@eltex.ru>
 */

#include	<stdio.h>
#include	<syslog.h>
#include	<sys/types.h>
#include	<sys/time.h>
#include	<sys/socket.h>
#include	<netdb.h>
#include	<netinet/in.h>
#include	<signal.h>
#include	<string.h>
#include	<strings.h>
#include	<errno.h>
#include	<stdlib.h>
#include	<inttypes.h>

#include	"firewall.h"
#include	"firewall2.h"
#include	"fwfunc.h"
#include	"auth.h"

static 	int	check_dest();

#define	MAXREQ		1023

static	char		**validests = (char **)0;
static	int		extendperm = 0;
static	int		cdscp = 0;
static	int		sdscp = 0;

static	fwparm	options[] = {
	{       FWPARM_BOOL,    "-extnd",       (char*) &extendperm       },
	{       FWPARM_LIST,    "-dest",        (char*) &validests        },
	{       FWPARM_STRING,  "-plug-to",     proxy_stats.dst           },
	{       FWPARM_PORT,  	"-port",     	(char*) &proxy_stats.port },
	{       FWPARM_STRING,  "-authuser",    proxy_stats.authuser      },
	{       FWPARM_DSCP,    "-client-dscp", (char*) &cdscp            },
	{       FWPARM_DSCP,    "-server-dscp", (char*) &sdscp            },
	{       0,              0,              0                         } };

static	int		srvport;

static	char		proxy_deny[] = "\
HTTP/1.0 403 Forbidden\r\n\
Content-Type: text/html\r\n\
\r\n\
<TITLE>Forbidden</TITLE>\n\
<H1>Proxy error: cannot connect to %s:%d</H1>\n\
Either forbidden destination or host %s unknown.";

static	char		proxy_error[] = "\
HTTP/1.0 500 Error\r\n\
Content-Type: text/html\r\n\
\r\n\
<TITLE>Error</TITLE>\n\
<H1>Proxy error: cannot connect to %s:%d</H1>\n\
%s.";

static	char		*client_headers[] = {
				"User-Agent: ",
				"Proxy-Connection: Keep-Alive",
				"Pragma: No-Cache",
				"Host: ",
				"Authorization: ",
				"Proxy-authorization: ",
				"Content-Length: 0",
				/* Other regular headers must go there */
				NULL
			};

int
main(argc,argv)
int	argc;
char	*argv[];
{
	Cfg		*cf;
	char		*port, *end;
	char		request[MAXREQ+1];
	char		buf[MAXREQ+1];
	char 		*srvnam;
	int		serfd;
	int		plug = 0;

	proxy_init(argc,argv);
	proxy_chroot_setugid();
	sosetline(SO_LCRLF);

	proxy_stats.port = 3128;

	if ((cf=proxy_conf_hosts(proxy_confp,proxy_stats.rladdr,
					proxy_stats.riaddr)))
		proxy_parse_options(cf,options);
	else
		exit(0);
	if (cdscp)
		proxy_set_dscp(0,cdscp);

	/* Netscape sends "CONNECT host:port HTTP/1.0\r\n" then headers */
	if(sogets(0, request, MAXREQ) <= 0)
		exit(1);
	request[strlen(request)-1] = '\0';
	if(strncasecmp(request,"connect ",8)
	 || (port = index(srvnam=xstrdup(request)+8,':')) == (char *)0
	 || (end = index(port,' ')) == (char *)0) {
		syslog(LLEV,"bad request %s",request);
		exit(1);
	}
	if (!*srvnam) {
		syslog(LLEV,"null destination");
		exit(1);
	}
	*end = *(port++) = 0;
	srvport = (uint16_t) atoi(port);

	if (*proxy_stats.dst)
		plug = 1;
	else {
		strncpy(proxy_stats.dst,srvnam,sizeof(proxy_stats.dst));
		proxy_stats.port = srvport;
	}

	/* Headers loop */
	while(1) {
		char	**p;
		char	*x;

		if(sogets(0, buf, MAXREQ) <= 0)
			exit(1);
		if ((x = index(buf,'\r')))
			*x = '\0';
		if(!buf[0])
			break;
		for (p = client_headers; *p; p++) {
			if (!strncasecmp(*p,buf,strlen(*p)))
				break;
		}
		if (! *p)
			syslog(LLEV,"unknown header %s",buf);
	}
	/* check to see if server is allowed */
	if (check_dest(srvnam,srvport)) {
		char	buf1[sizeof(proxy_deny)+2*MAXREQ];

		snprintf(buf1,sizeof(buf1),proxy_deny,proxy_stats.dst,srvport,proxy_stats.dst);
		sosay(0,buf1);
		exit(1);
	}

	if (extendperm) {
		char	buf1[sizeof(proxy_deny)+2*MAXREQ];
		snprintf(buf1,sizeof(buf1),proxy_deny,proxy_stats.dst,srvport,proxy_stats.dst);
		switch (auth_perm(proxy_confp,proxy_stats.authuser,proxy_name,
			srvnam,NULL)) {
		case -1:
			syslog(LLEV,"no match for %.256s user=%.100s in netperm-table",
					proxy_name,proxy_stats.authuser);
			sosay(0,buf1);
			exit(1);
		case 1:
			syslog(LLEV,"deny host=%.512s/%.20s connect to %.512s user=%.100s",
					proxy_stats.rladdr,proxy_stats.riaddr,
					srvnam,proxy_stats.authuser);
			sosay(0,buf1);
			exit(1);
		}
	}
			
	proxy_update_status();

	if (plug)
		syslog(LLEV,"permit host=%s/%s connect to %s:%d via=%s:%d",
				proxy_stats.rladdr,proxy_stats.riaddr,srvnam,
				srvport,proxy_stats.dst,proxy_stats.port);
	else
		syslog(LLEV,"permit host=%s/%s connect to %s:%d",
				proxy_stats.rladdr,proxy_stats.riaddr,srvnam,
				srvport);

	if((serfd = conn_server(proxy_stats.dst,proxy_stats.port,0,NULL)) < 0) {
		char	buf1[sizeof(proxy_error)+MAXREQ+512];

		snprintf(buf1,sizeof(buf1),proxy_error,proxy_stats.dst,proxy_stats.port,
				strerror(errno));
		syslog(LLEV,"cannot connect to server %s:%d: %s",
				proxy_stats.dst,srvport,strerror(errno));
		sosay(0,buf1);
		exit(1);
	}
	if (sdscp)
		proxy_set_dscp(serfd,sdscp);

	if (plug) {
		char buf1[512];
		snprintf(request,MAXREQ,"CONNECT %s:%u HTTP/1.0\r\n",
				srvnam,srvport);
		if (sosay(serfd,request)) {
			syslog(LLEV,"fwtksyserr: parent proxy communication error %s",strerror(errno));
			exit(1);
		}
		proxy_stats.outbytes += strlen(request);
		snprintf(buf1,64,"CONNECT %s:%u",
				srvnam,srvport);
		buf1[sizeof(buf1)-1] = '\0';
		proxy_update_operation(buf1);
	} else {
		if(sosay(0,"HTTP/1.0 200 Ok\r\n")) {
			syslog(LLEV,"write client: %s",strerror(errno));
			proxy_exit();
		}
	}

	proxy_tunnel(0,serfd);

/*	We want final accounting to list real destination, not the parent proxy
 */
	if (plug) {
		strncpy(proxy_stats.dst,srvnam,sizeof(proxy_stats.dst)-1);
		proxy_stats.port = srvport;
	}

	proxy_exit();
	exit(-1); /* not reached */
}

/*
 * next 2 functions (pretty generic though) are taken from Edelweb's ssl-gw 
 * by Jean-Cristophe Touvet <jct@edelweb.fr> 
 */


static	int
hostportmatch(pattern,string,port)
char	*pattern;
char	*string;
int	port;
{
	char	*p;

	if ((p = index (pattern,':')) != (char *)0)
		*p = 0;		/* Put \0 if port number(s)	*/
	if (! hostmatch(pattern,string)) {
		/* Host doesn't match: return false now	*/
		if (p) *p = ':';	/* Put ':' back	*/
		return 0;
	}
	/* Host matches */
	if (! p) return 1;	/* No port number(s): return true	*/
	*p = ':';	/* Put ':' back	*/
	do {
		if (! *++p) {
			syslog(LLEV,"fwtkcfgerr: port number expected (%s)",pattern);
			exit (1);
		}
		if (port == atoi (p))
			return 1;
	} while ((p = index (p,':')) != (char *)0);
	return 0;
}

static	int
check_dest(char* srv,int port)
{
	char **xp;
	int denied;

	if((denied = (validests != NULL))) {
		for(xp = validests; *xp != NULL; xp++)
			if( **xp == '!' && hostportmatch(*xp+1,srv,port))
				break;
			else
				if(hostportmatch(*xp, srv, port)) {
					denied = 0;
					break;
				}
	}

	if (denied)
		syslog(LLEV,"deny host=%s/%s connect to %s:%d", proxy_stats.rladdr, proxy_stats.riaddr, proxy_stats.dst, srvport);
	return denied;
}

