/*

	phpsocks - an extension to php for use with socks proxies
	Copyright (C) 2000 Paolo Prandini

	liberally derived from:
		TSOCKS - Wrapper library for transparent SOCKS 
		Copyright (C) 2000 Shaun Clowes 

	

    This program 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.

    This program 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 this program; if not, write to the Free Software
    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.

*/

/* PreProcessor Defines */
#define DEBUG		1		   /* Show error messages?     */
//#define DEBUG

/* Header Files */
#include "php.h"
#include "php_ini.h"

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <dlfcn.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pwd.h>
#include <errno.h>
#include <fcntl.h>
#include <stdarg.h>

struct socksent {
        struct in_addr localip;
        struct in_addr localnet;
	struct socksent *next;
};

struct sockaddr_link {
        struct sockaddr_in ip;
	struct sockaddr_link *next;
};

struct sockreq {
        int8_t version;
        int8_t command;
        int16_t dstport;
        int32_t dstip;
        /* A null terminated username goes here */
};

struct sockrep {
        int8_t version;
        int8_t result;
        int16_t ignore1;
        int32_t ignore2;
};

/* Global Declarations */
static struct socksent *localnets = NULL; 
static int initialized = 0;
static int servertype = 0;
static int servervalid = 0;
static unsigned short int sin_port;
static struct sockaddr_link *servers = NULL;
static struct sockaddr_link *currentserver = NULL;
static char *default_user = NULL;
static char *default_pass = NULL;

/* Exported Function Prototypes */
int Tconnect (int, __CONST_SOCKADDR_ARG, socklen_t);

/* Private Function Prototypes */
static int initialize();
static int read_config();
static int handle_server(char *);
static int handle_type(char *);
static int handle_port(char *);
static int handle_local(char *);
static int handle_defuser(char *);
static int handle_defpass(char *);
static int is_local(struct in_addr *);
static int connect_socks(int, struct sockaddr_in *);
static int connect_socksv4(int, struct sockaddr_in *, struct sockaddr_in *);
static int connect_socksv5(int, struct sockaddr_in *, struct sockaddr_in *);
static void show_error(char *, ...);

static int initialize () {

	/* Read in the config file */
	read_config();

	initialized = 1;

	return(0);
}

static int read_config () {

	int cleardefs = 0;
	struct sockaddr_link *sal;

	handle_server(INI_STR("socks.server"));
	handle_port(INI_STR("socks.port"));
	handle_type(INI_STR("socks.type"));
	handle_defuser(INI_STR("socks.default_user"));
	handle_defpass(INI_STR("socks.default_pass"));
	handle_local(INI_STR("socks.local"));

	/* Always add the 127.0.0.1/255.0.0.0 subnet to local */
	handle_local("127.0.0.0/255.0.0.0");

	for (sal=servers;sal!=NULL;sal=sal->next) 
	if (is_local(&sal->ip.sin_addr)) {
		show_error("SOCKS server is not on a local subnet!\n");
		sal->ip.sin_family=0;
	} else servervalid=1;

	if (servers == NULL) {
		show_error("No valid SOCKS server specified in "
			   "configuration file\n");
		servervalid=0;
	} 

	/* Default to the default SOCKS port */
	if (sin_port == 0) {
		sin_port = htons(1080);
	}

	/* Default to SOCKS V4 */
	if (servertype == 0) {
		servertype = 4;
	}

	if ((default_user != NULL) || (default_pass != NULL)) {
		/* If the server type is version 4, neither */
		/* username or password is valid */
		if (servertype == 4) {
			show_error("Default username/pass "
				   "only valid for version 5 "
				   "SOCKS servers\n");
			cleardefs = 1;
		} else 
		/* If default user and pass not specified */
		/* together then show an error */
		if (!((default_user != NULL) && 
		      (default_pass != NULL))) {
			show_error("Default username and "
				   "pass MUST be specified "
				   "together\n");
			cleardefs = 1;
		}
		if (cleardefs == 1) {
			if (default_user != NULL) {
				free(default_user);
				default_user = NULL;
			}
			if (default_pass != NULL) {
				free(default_user);
				default_user = NULL;
			}
		}
	}

	return(0);
}

static int 
handle_server(char *value) 
{
	struct sockaddr_link *ent;
	char *ip,*srv;

	while (1) {

		// Process multiple servers delimited by "," symbol
		srv = strsep(&value, ",");
		if (srv==NULL) srv=value;
		if (srv==NULL) break;

		ip = strsep(&srv, " ");
		if (ip==NULL) break;

        	ent=(struct sockaddr_link *)malloc(sizeof(struct sockaddr_link));
		if (ent==NULL) {
			/* if we couldn't malloc some storage, leave */
			exit(-1);
		}
		if (!(inet_aton(ip, &ent->ip.sin_addr))) {
			show_error("The SOCKS server (%s) in configuration "
			   "file is invalid\n", ip);
			free(ent);
		} else {		
			/* Construct the addr for the socks server */
			ent->ip.sin_family = AF_INET; /* host byte order */
        		ent->ip.sin_port = 0;
			bzero(&(ent->ip.sin_zero), 8);/* zero the rest of the struct */    
			ent->next=servers;
			servers=ent;
		}
	}

	return(0);
}

static int 
handle_port(char *value) 
{

	if (sin_port != 0) {
		show_error("Server port may only be specified once "
			   "in configuration file\n");
	} else {
		errno = 0;
		sin_port = (unsigned short int)
				  htons(strtol(value, (char **)NULL, 10));
		if ((errno != 0) || (sin_port == 0)) {
			show_error("Invalid server port number "
				   "specified in configuration file "
				   "(%s)\n", value);
			sin_port = 0;
		}
	}
	
	return(0);
}

static int 
handle_defuser(char *value) 
{

	if (default_user != NULL) {
		show_error("Default username may only be specified "
			   "once in configuration file\n");
	} else {
		default_user = strdup(value);
	}
		
	return(0);
}

static int 
handle_defpass(char *value) 
{

	if (default_pass != NULL) {
		show_error("Default password may only be specified "
			   "once in configuration file\n");
	} else {
		default_pass = strdup(value);
	}
	
	return(0);
}

static int 
handle_type(char *value) 
{

	if (servertype != 0) {
		show_error("Server type may only be specified once "
			   "in configuration file\n");
	} else {
		errno = 0;
		servertype = (int) strtol(value, (char **)NULL, 10);
		if ((errno != 0) || (servertype == 0) ||
		    ((servertype != 4) && (servertype != 5))) {
			show_error("%d %d\n", errno, servertype);
			show_error("Invalid server type "
				   "specified in configuration file "
				   "(%s) only 4 or 5 may be "
				   "specified\n", value);
			servertype = 0;
		}
	}

	return(0);
}

static int 
handle_local(char *value) 
{
	char *ip;
	char *subnet;
	char buf[100];
	char *split,*net;
	struct socksent *ent;

	while (1) {

		// Process multiple nets delimited by "," symbol
		net = strsep(&value, ",");
		if (net==NULL) net=value;
		if (net==NULL) break;

		strncpy(buf, net, sizeof(buf) - 1);
		split = buf;
		ip = strsep(&split, "/");
		subnet = strsep(&split, " \n");

		if ((ip == NULL) && (subnet == NULL)) break;

		if ((ip == NULL) || (subnet == NULL)) {
			show_error("Local network pair (%s) is not validly "
				   "constructed\n", value);
			return(1);
		} else {
			/* Allocate the new entry */
			if ((ent = (struct socksent *) malloc(sizeof(struct socksent)))
		   == NULL) {
				/* If we couldn't malloc some storage, leave */
				exit(-1);
			}

			if (!(inet_aton(ip, &(ent->localip)))) {
				show_error("In configuration file IP for local "
					   "network (%s) is not valid\n", ip);
				free(ent);
				return(1);
			} 
			else if ((ent->localnet.s_addr = inet_addr(subnet)) == -1) {
				show_error("In configuration file SUBNET for " 
					   "local network (%s) is not valid\n", ip);
				free(ent);
				return(1);
			}
			else if ((ent->localip.s_addr &
				  ent->localnet.s_addr) != 
	                	 ent->localip.s_addr) {
				show_error("In configuration file IP (%s) & "
					   "SUBNET (%s) != IP. Ignored.\n",
				   	ip, subnet);
				free(ent);
				continue;
			} else {
				/* The entry is valid so add to linked list */
				ent -> next = localnets;
				localnets = ent;
			}			
		}
	}
	return(0);
}
	
	
static int is_local(struct in_addr *testip) {
	struct socksent *ent;

	for (ent = localnets; ent != NULL; ent = ent -> next) {
		if ((testip->s_addr & ent->localnet.s_addr) ==
		    (ent->localip.s_addr & ent->localnet.s_addr))  {
			return(0);
		}
	}

	return(1);
}

int Tconnect (int __fd, __CONST_SOCKADDR_ARG __addr, socklen_t __len) {
	struct sockaddr_in *connaddr;
	void **kludge;
	int sock_type = -1;
	int sock_type_len = sizeof(sock_type);

	/* If we haven't initialized yet, do it now */
	if (initialized == 0) {
		initialize();
	}

	/* Ok, so this method sucks, but it's all I can think of */
	kludge = (void *) & __addr; 
	connaddr = (struct sockaddr_in *) *kludge;

	/* Get the type of the socket */
	getsockopt(__fd, SOL_SOCKET, SO_TYPE, 
		   (void *) &sock_type, &sock_type_len);

	/* Only try and use socks if the socket is an INET socket, */
	/* is a TCP stream, isn't on a local subnet and the        */
	/* socks server in the config was valid                    */
	if ((connaddr->sin_family != AF_INET) ||
	    (sock_type != SOCK_STREAM) ||
	    (servervalid == 0) || 
	    !(is_local(&(connaddr->sin_addr)))) {
		return(connect(__fd, __addr, __len));
	} else {
		return(connect_socks(__fd, connaddr));
	} 

}

static int connect_socks(int sockid, struct sockaddr_in *connaddr) {
	int rc = 0;
	int sockflags;
	struct sockaddr_link *sal;
	
	if (currentserver==NULL) currentserver=servers;
	sal=currentserver;

	do {

		/* Get the flags of the socket, (incase its non blocking */
		if ((sockflags = fcntl(sockid, F_GETFL)) == -1) {
			sockflags = 0;
		}

		/* If the flags show the socket as blocking, set it to   */
		/* blocking for our connection to the socks server       */
		if ((sockflags & O_NONBLOCK) != 0) {
			fcntl(sockid, F_SETFL, sockflags & (~(O_NONBLOCK)));
		}

		if (servertype == 4) 
			rc = connect_socksv4(sockid, connaddr, &sal->ip);
		else
			rc = connect_socksv5(sockid, connaddr, &sal->ip);

		/* If the socket was in non blocking mode, restore that */
		if ((sockflags & O_NONBLOCK) != 0) {
			fcntl(sockid, F_SETFL, sockflags);
		}	

		if (rc==0) break;
		if (rc==ECONNREFUSED) break;

		sal=sal->next;
		if (!sal) sal=servers;

	} while (sal!=currentserver);

	if (rc != 0) {
		errno = rc;
		return(-1);
	} else {
		currentserver=sal;
	}
	return(0);   

}			

static int connect_socksv4(int sockid, struct sockaddr_in *connaddr, struct sockaddr_in *server) {
	int rc = 0;
	int length = 0;
	char *realreq;
	struct passwd *user;
	struct sockreq *thisreq;
	struct sockrep thisrep;
	int tmpsock;
	
	if (server->sin_port==0) server->sin_port=sin_port;
	if (server->sin_family==0) return ECONNREFUSED;

	/* Determine the current username */
	user = getpwuid(getuid());	

	/* Allocate enough space for the request and the null */
	/* terminated username */
	length = sizeof(struct sockreq) + 
			(user == NULL ? 1 : strlen(user->pw_name) + 1);
	if ((realreq = malloc(length)) == NULL) {
		/* Could not malloc, bail */
		exit(1);
	}
	thisreq = (struct sockreq *) realreq;

	/* Create the request */
	thisreq->version = 4;
	thisreq->command = 1;
	thisreq->dstport = connaddr->sin_port;
	thisreq->dstip   = connaddr->sin_addr.s_addr;

	/* Copy the username */
	strcpy(realreq + sizeof(struct sockreq), 
	       (user == NULL ? "" : user->pw_name));

	/* open a test socket */
	tmpsock=socket(AF_INET,SOCK_STREAM,0);
	if ((rc = connect(tmpsock, (struct sockaddr *) server, 
		              sizeof(struct sockaddr_in))) != 0) {
		show_error("Error %d attempting to test connect to SOCKS "
				"server %s:%d\n", errno, 
				inet_ntoa(server->sin_addr),
				ntohs(server->sin_port));
		close(tmpsock);
		return(rc);
	}
	shutdown(tmpsock,2);
	close(tmpsock);

	/* Connect this socket to the socks server instead */
	if ((rc = connect(sockid, (struct sockaddr *) server, 
		              sizeof(struct sockaddr_in))) != 0) {
		show_error("Error %d attempting to connect to SOCKS "
				   "server\n", errno);
		rc = rc;
	} else {
		rc = send(sockid, (void *) thisreq, length,0);
		if (rc < 0) {
			show_error("Error %d attempting to send SOCKS "
				   "request\n", errno);
			rc = rc;
		} else if ((rc = recv(sockid, (void *) &thisrep, 
				      sizeof(struct sockrep), 0)) < 0) {
			show_error("Error %d attempting to receive SOCKS "
				   "reply\n", errno);
			rc = ECONNREFUSED;
		} else if (rc < sizeof(struct sockrep)) {
			show_error("Short reply from SOCKS server\n");
			/* Let the application try and see how they */
			/* go                                       */
			rc = 0;
		} else if (thisrep.result == 91) {
			show_error("SOCKS server refused connection\n");
			rc = ECONNREFUSED;
		} else if (thisrep.result == 92) {
			show_error("SOCKS server refused connection "
				   "because of failed connect to identd "
				   "on this machine\n");
			rc = ECONNREFUSED;
		} else if (thisrep.result == 93) {
			show_error("SOCKS server refused connection "
				   "because identd and this library "
				   "reported different user-ids\n");
			rc = ECONNREFUSED;
		} else {
			rc = 0;
		}
	}

	/* Free the SOCKS request storage that was malloced */
	free(realreq);

	return(rc);   

}			

static int connect_socksv5(int sockid, struct sockaddr_in *connaddr, struct sockaddr_in *server) {
	int rc = 0;
	int offset = 0;
	char *verstring = "\x05\x02\x02\x00";
	char *uname, *upass;
	struct passwd *nixuser;
	char buf[200];
	int tmpsock;

	if (server->sin_port==0) server->sin_port=sin_port;
	if (server->sin_family==0) return ECONNREFUSED;

	/* open a test socket */
	tmpsock=socket(AF_INET,SOCK_STREAM,0);
	if ((rc = connect(tmpsock, (struct sockaddr *) server, 
		              sizeof(struct sockaddr_in))) != 0) {
		show_error("Error %d attempting to test connect to SOCKS "
				"server %s:%d\n", errno, 
				inet_ntoa(server->sin_addr),
				ntohs(server->sin_port));
		close(tmpsock);
		return(rc);
	}
	shutdown(tmpsock,2);
	close(tmpsock);

	/* Connect this socket to the socks server */
	if ((rc = connect(sockid, (struct sockaddr *) server, 
		              sizeof(struct sockaddr_in))) != 0) {
		show_error("Error %d attempting to connect to SOCKS "
				"server %s:%d\n", errno, 
				inet_ntoa(server->sin_addr),
				ntohs(server->sin_port));
		return(rc);
	}

        /* Now send the method negotiation */
	if ((rc = send(sockid, (void *) verstring, 4,0)) < 0) {
		show_error("Error %d attempting to send SOCKS "
			   "method negotiation\n", errno);
		return(rc);
	}

	/* Now receive the reply as to which method we're using */
	if ((rc = recv(sockid, (void *) buf, 2, 0)) < 0) {
		show_error("Error %d attempting to receive SOCKS "
			   "method negotiation reply\n", errno);
		rc = ECONNREFUSED;
		return(rc);
	}

	if (rc < 2) {
		show_error("Short reply from SOCKS server\n");
		rc = ECONNREFUSED;
		return(rc);
	}

	/* See if we offered an acceptable method */
	if (buf[1] == '\xff') {
		show_error("SOCKS server refused authentication methods\n");
		rc = ECONNREFUSED;
		return(rc);
	}

	/* If the socks server chose username/password authentication */
	/* (method 2) then do that */
	if ((unsigned short int) buf[1] == 2) {

		/* Determine the current *nix username */
		nixuser = getpwuid(getuid());	

		if (
		    ( strlen(uname = INI_STR("socks.user")) == 0) &&
		    ((uname = getenv("TSOCKS_USERNAME")) == NULL) &&
		    ((uname = default_user) == NULL) &&
		    ((uname = (nixuser == NULL ? NULL : nixuser->pw_name)) == NULL)) {
			show_error("Could not get SOCKS username from "
				   "local passwd file, php.ini "
				   "or $TSOCKS_USERNAME to authenticate "
				   "with"); 
			rc = ECONNREFUSED;
			return(rc);
		} 

		if (
		    ( strlen(upass = INI_STR("socks.pass")) == 0) &&
		    ((upass = getenv("TSOCKS_PASSWORD")) == NULL) &&
		    ((upass = default_pass) == NULL)) {
			show_error("Need a password in php.ini or "
				   "$TSOCKS_PASSWORD to authenticate with");
			rc = ECONNREFUSED;
			return(rc);
		} 
		
		offset = 0;
		buf[offset] = '\x01';
		offset++;
		buf[offset] = (int8_t) strlen(uname);
		offset++;
		memcpy(&buf[offset], uname, strlen(uname));
		offset = offset + strlen(uname);
		buf[offset] = (int8_t) strlen(upass);
		offset++;
		memcpy(&buf[offset], upass, strlen(upass));
		offset = offset + strlen(upass);

		/* Send out the authentication */
		if ((rc = send(sockid, (void *) buf, offset,0)) < 0) {
			show_error("Error %d attempting to send SOCKS "
				   "authentication\n", errno);
			return(rc);
		}


		/* Receive the authentication response */
		if ((rc = recv(sockid, (void *) buf, 2, 0)) < 0) {
			show_error("Error %d attempting to receive SOCKS "
				   "authentication reply\n", errno);
			rc = ECONNREFUSED;
			return(rc);
		}

		if (rc < 2) {
			show_error("Short reply from SOCKS server\n");
			rc = ECONNREFUSED;
			return(rc);
		}

		if (buf[1] != '\x00') {
			show_error("SOCKS authentication failed, "
				   "check username and password\n");
			rc = ECONNREFUSED;
			return(rc);
		}
		
	} 

	/* Now send the connect */
	buf[0] = '\x05';		/* Version 5 SOCKS */
	buf[1] = '\x01'; 		/* Connect request */
	buf[2] = '\x00';		/* Reserved	   */
	buf[3] = '\x01';		/* IP version 4    */
	memcpy(&buf[4], &connaddr->sin_addr.s_addr, 4);
	memcpy(&buf[8], &connaddr->sin_port, 2);

        /* Now send the connection */
	if ((rc = send(sockid, (void *) buf, 10,0)) < 0) {
		show_error("Error %d attempting to send SOCKS "
			   "connect command\n", errno);
		return(rc);
	}

	/* Now receive the reply to see if we connected */
	if ((rc = recv(sockid, (void *) buf, 10, 0)) < 0) {
		show_error("Error %d attempting to receive SOCKS "
			   "connection reply\n", errno);
		rc = ECONNREFUSED;
		return(rc);
	}

	if (rc < 10) {
		show_error("Short reply from SOCKS server\n");
		rc = ECONNREFUSED;
		return(rc);
	}

	/* See the connection succeeded */
	if (buf[1] != '\x00') {
		show_error("SOCKS connect failed: ");
		switch ((int8_t) buf[1]) {
			case 1:
				show_error("General SOCKS server failure\n");
				return(ECONNABORTED);
			case 2:
				show_error("Connection denied by rule\n");
				return(ECONNABORTED);
			case 3:
				show_error("Network unreachable\n");
				return(ENETUNREACH);
			case 4:
				show_error("Host unreachable\n");
				return(EHOSTUNREACH);
			case 5:
				show_error("Connection refused\n");
				return(ECONNREFUSED);
			case 6: 
				show_error("TTL Expired\n");
				return(ETIMEDOUT);
			case 7:
				show_error("Command not supported\n");
				return(ECONNABORTED);
			case 8:
				show_error("Address type not supported\n");
				return(ECONNABORTED);
			default:
				show_error("Unknown error\n");
				return(ECONNABORTED);
		}	
	}

	return(0);   

}			

static void show_error(char *fmt, ...) {
#ifdef DEBUG
	va_list ap;
	int saveerr;
	char *newfmt;
	FILE *fp;
	char MYNAME[]= "phpsocks: ";

	if ((newfmt = malloc(strlen(fmt) + strlen(MYNAME) + 1)) == NULL) {
		/* Could not malloc, bail */
		exit(1);
	}
	
	strcpy(newfmt, MYNAME);
	strcpy(newfmt + strlen(MYNAME), fmt);

	va_start(ap, fmt);

	/* Save errno */
	saveerr = errno;

	fp=fopen("/tmp/sock.log","a");
	if (fp!=NULL) {
		vfprintf(fp, newfmt, ap);
		fclose(fp);
	}
	
	errno = saveerr;

	va_end(ap);

	free(newfmt);
#endif
}
	
