#!/usr/bin/python -u
"""
    Pytecache is a GnutellaNet hosts cachter cacher servant.
    Copyright (C) 2000  Tom Goulet

    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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

    I can be reached at tomg@nova.yi.org.
"""
# User changeable defines.
PORT = 6346				# Port Pytecache will listen on.
MAXCONNS = 16				# Maximum number of connections.
MAXPONGS = 64				# Maximum number of pongs to be sent.
# You shouldn't need to change anything else.


# Modules
import socket				# It's a TCP/IP program...figure it out
import select				# To tell if a socket is ready.
import time				# For sleep and timing stuff.
import string				# For hostname string manipulation
import os				# To get the user's home directory.
import signal				# To make timeouts possible.

# Exceptions
Timeout = 'Timeout'

# Defines
# Keys for a connection entry.
SOCK = 0				# The socket object.
FLAG = 1				# Connection flag.
GUID = 2				# GUID of the ping.
ADDR = 3				# Address of the host.
TIME = 4				# A timestamp for whatever use.

# Possible flags for the connection FLAG key.
CLOSE = -1				# Close and remove.
DUPE = 0				# Check for duplicates.
HAND = 1				# Receive handshake.
WELC = 2				# Send welcome.
PING = 3				# Receive ping.
PONG = 4				# Send pong.

def main(s):
	hostsfile = os.environ['HOME']+'/.gnut_hosts'
	f = open(hostsfile, 'r')
	conns = []
	print "Pytecache running on localhost:6346"
	while 1:
		# Connections FLAGed CLOSE are close()d and remove()d.
                i = 0
                while i < len(conns):
                        if conns[i][FLAG] == CLOSE:
                                print "Closing connection:", conns[i][ADDR][0]
                                conns[i][SOCK].close()
                                conns.remove(conns[i])
                        else:
                                i = i + 1
		# If there is a new connection on the listening socket...
	        if select.select([s], [], [], 0)[0] == [s]:
			conns = accept(s, conns)
		# deal with every connection in conns.
		map(handleconn, conns, [f]*len(conns))
		# So I don't max the cpu.
		time.sleep(0.05)

def accept(s, conns):
	if len(conns) > MAXCONNS:
		print conns[0][ADDR][0], time.strftime('%a %b %d %H:%M:%S %Z %Y',time.localtime(time.time())), 'Maximum connections reached.'
		conns[0][SOCK].close()
		conns.remove(conns[0])
	try:
		sock, addr = s.accept()
	except:
		return conns
	print "Incoming connection:", addr[0]
	for i in conns:
		if addr[0] == i[ADDR][0]:
			sock.close()
			print "Duplicate address, closing:", addr[0]
			return conns
	conns.append({SOCK: sock, ADDR: addr, FLAG: HAND})
	return conns

def handler(signum, frame):
	signal.alarm(0)
	raise Timeout
        return

def recv(conn, timeout, length):
	signal.signal(signal.SIGALRM, handler)
	signal.alarm(timeout)
	try: data = conn.recv(length)
	finally: signal.alarm(0)
	return data

def send(conn, timeout, data):
	signal.signal(signal.SIGALRM, handler)
	signal.alarm(timeout)
	conn.send(data)
	signal.alarm(0)
	return

def handleconn(conn, f):
	if conn[FLAG] == CLOSE:
		# This shouldn't happen.
		print "connection with flag of CLOSE sent to handleconn"
		pass
	elif conn[FLAG] == HAND:
		if select.select([conn[SOCK]], [], [], 0)[0] == [conn[SOCK]]:
			try:
				handshake = recv(conn[SOCK], 1, 22)
				if handshake == 'GNUTELLA CONNECT/0.4\n\n':
					conn[FLAG] = WELC
				else:
					conn[FLAG] = CLOSE
			except:
				signal.alarm(0)
				conn[FLAG] = CLOSE
	elif conn[FLAG] == WELC:
		countersign = 'GNUTELLA OK\n\n'
		try:
			send(conn[SOCK], 1, countersign)
			conn[FLAG] = PING
		except:
			signal.alarm(0)
			conn[FLAG] = CLOSE
	elif conn[FLAG] == PING:
		if select.select([conn[SOCK]], [], [], 0)[0] == [conn[SOCK]]:
			try:
				rheader = recv(conn[SOCK], 1, 23)
				guid = rheader[0:16]
				func = ord(rheader[16])
				hops = ord(rheader[18])
				# Packet must be a ping with 0 hops.
				if func == 0 and hops == 0:
					conn[FLAG] = PONG
					conn[GUID] = guid
				else:
					conn[FLAG] = CLOSE
			except:
				signal.alarm(0)
				conn[FLAG] = CLOSE
	elif conn[FLAG] in range(PONG, PONG+MAXPONGS):
                host = string.strip(f.readline())
                if host == '':
			f.seek(0)
			host = string.strip(f.readline())
		host = string.split(host)
		if validhost(host):
	                host = (host[0], int(host[1]))
			s = conn[SOCK]
			rawipaddress = ''
			for i in string.split(host[0], '.'):
				rawipaddress = rawipaddress + chr(int(i))
			rawport = chr(host[1]%256)+chr(host[1]/256)
			sheader = conn[GUID] + '\001\000\000\016\000\000\000'
			spayload = rawport + rawipaddress + 8*'\000'
			spacket = sheader + spayload
			try:
				send(s, 1, spacket)
	                except:
				signal.alarm(0)
				conn[FLAG] = CLOSE
			conn[FLAG] = conn[FLAG] + 1
	elif conn[FLAG] == PONG+MAXPONGS:
		print "Pong flood sent:", conn[ADDR][0]
		conn[FLAG] = CLOSE
	else:
		print "Unknown flag:", conn
	return(conn)

def validhost(host):
        # We check for the validity of a host.
	addy = string.split(host[0], '.')
	if host[1] == 0: return 0	# Port is 0
	elif addy[0] == '10': return 0	# Private IP
        elif addy[0] == '192': return 0	# Private IP
        elif addy[0] == '172' and int(addy[1]) in range(16, 32):
			return 0	# Private IP
        elif addy[0] == '0': return 0	# Reserved IP
        elif addy[0] == '1': return 0	# Reserved IP
        elif addy[0] == '2': return 0	# Reserved IP
        elif addy[0] == '127': return 0	# Loopback IP
        elif addy[3] == '0': return 0	# Invalid IP
        elif '255' in addy: return 0	# Broadcast IP
	# Host appears to be valid.
	else: return 1

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind('', PORT)
s.listen(1)
try: main(s)
except:
	s.close()
	raise
