#!/usr/bin/env python
"""
    Pytecache is a Gnutella hosts cache 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.
"""
PORT       = 6346
MAX_CONNS  = 16
MAX_PONGS  = 1024
HOSTS_FILE = "/home/gnutella/.gnut_hosts"
VERSION = "1.6"

from socket import socket, AF_INET, SOCK_STREAM, SOL_SOCKET, SO_REUSEADDR, \
	error
socket_error = error ; del error
from SOCKET import SOMAXCONN
SHUT_RD = 0
from select import select
from posixfile import SEEK_END
from struct import pack, unpack
from whrandom import randint
from string import join, split
from time import ctime, time, sleep

HANDSHAKE  = "GNUTELLA CONNECT/0.4\n\n"
WELCOME    = "GNUTELLA OK\n\n"
PONG_SIZE  = 37
PING_SIZE  = 23
BUFSIZE    = 65536

CF_NONE    = 0			# No connection
CF_HAND    = 1			# Receive Gnutella handshake
CF_PING    = 2			# Receive Genny welcome
CF_PONG    = 3			# Send more pongs

class pytecache_conn:
	pass

def main():
	connections = []
	listen_sock = socket(AF_INET, SOCK_STREAM)
	listen_sock.setblocking(0)
	listen_sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
	listen_sock.bind(('', PORT))
	listen_sock.listen(SOMAXCONN)
	print "%s  Pytecache %s ready, listening on 0.0.0.0:%s/tcp" % \
		(ctime(time()), VERSION, PORT)
	while 1:
		readfds = [listen_sock]
		writefds = []
		i = 0
		while i < len(connections):
			if connections[i].flag == CF_NONE:
				connections.remove(connections[i])
				continue
			i = i + 1
		one_minute_ago = int(time()) - 60
		for conn in connections:
                        if conn.time < one_minute_ago:
                                conn.close()
				if conn.flag == CF_HAND:  strflag = 'CF_HAND'
				elif conn.flag == CF_PING:  strflag = 'CF_PING'
				elif conn.flag == CF_PONG:  strflag = 'CF_PONG'
                                conn.flag = CF_NONE
                                print "%s  %s:%s stale connection, flag: %s" % \
					(ctime(time()), conn.addr[0], \
					conn.addr[1], strflag)
			if conn.flag == CF_HAND or conn.flag == CF_PING:
				readfds.append(conn)
			else:
				writefds.append(conn)
		readfds, writefds = select(readfds, writefds, [])[0:2]
		for conn in readfds + writefds:
			if conn is listen_sock:
				add_connection(listen_sock, connections)
			elif conn.flag == CF_HAND:
				handle_handshake(conn)
			elif conn.flag == CF_PING:
				handle_ping(conn)
			else:
				send_pongs(conn)

def add_connection(listen_sock, connections):
	sock, addr = listen_sock.accept()
	sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
	for conn in connections:
		if conn.addr[0] == addr[0]:
			sock.close()
			print "%s  %s:%s duplicate address" % (ctime(time()), \
				addr[0], addr[1])
			return
	if len(connections) == MAX_CONNS:
		sock.close()
		print "%s  %s:%s maximum connections reached" % \
			(ctime(time()), addr[0], addr[1])
		return
	sock.setblocking(0)
	conn = pytecache_conn()
	conn.addr = addr
	conn.fileno = sock.fileno
	conn.close = sock.close
	conn.send = sock.send
	conn.recv = sock.recv
	conn.time = int(time())
	conn.shutdown = sock.shutdown
	conn.flag = CF_HAND
	connections.append(conn)
	return

def handle_handshake(conn):
	try:
		handshake = conn.recv(BUFSIZE)
	except socket_error, (errno, strerror):
		conn.close()
		conn.flag = CF_NONE
		print "%s  %s:%s %s in handle_handshake recv" % \
			(ctime(time()), conn.addr[0], conn.addr[1], strerror)
		return
	if handshake != HANDSHAKE:
		conn.close()
		conn.flag = CF_NONE
		print "%s  %s:%s bad handshake: %s" % (ctime(time()), \
			conn.addr[0], conn.addr[1], `handshake`)
		return
	try:
		conn.send(WELCOME)
	except socket_error, (errno, strerror):
		conn.close()
		conn.flag = CF_NONE
		print "%s  %s:%s %s in handle_handshake send" % \
			(ctime(time()), conn.addr[0], conn.addr[1], strerror)
		return
	conn.flag = CF_PING
	conn.time = int(time())
	return

def handle_ping(conn):
	try:
		ping = conn.recv(BUFSIZE)
	except socket_error, (errno, strerror):
		conn.close()
		conn.flag = CF_NONE
		print "%s  %s:%s %s in handle_handshake send" % \
			(ctime(time()), conn.addr[0], conn.addr[1], strerror)
		return
	if len(ping) != PING_SIZE:
		conn.close()
		conn.flag = CF_NONE
		print "%s  %s:%s ping wrong size: %s" % (ctime(time()), \
			 conn.addr[0], conn.addr[1], len(ping))
		return
	if ord(ping[16]) != 0:
		conn.close()
		conn.flag = CF_NONE
		print "%s  %s:%s bad function: %i" % (ctime(time()), \
			conn.addr[0], conn.addr[1], ord(ping[16]))
		return
        if ord(ping[18]) != 0:
                conn.close()
                conn.flag = CF_NONE
                print "%s  %s:%s bad hops count: %i" % (ctime(time()), \
			conn.addr[0], conn.addr[1], ord(ping[18]))
                return
	if unpack('L', ping[19:23])[0] != 0:
                conn.close()
                conn.flag = CF_NONE
                print "%s  %s:%s bad payload length: %i" % (ctime(time()), \
			conn.addr[0], conn.addr[1], unpack('L', ping[19:23])[0])
		return
	conn.shutdown(SHUT_RD)
	conn.pongs = construct_pongs(ping[0:16])
	conn.p = 0
	conn.flag = CF_PONG
	send_pongs(conn)
	return

def construct_pongs(guid):
	f = open(HOSTS_FILE, 'r')
	f.seek(0, SEEK_END)
	f.seek(randint(0, f.tell()))
	f.readline()		# Go to the beginning of a new line
	pongs = []
	while len(pongs) != MAX_PONGS:
		host_line = f.readline()
		if not host_line:
			f.seek(0)
			host_line = f.readline()
		try:
			ipaddy, port = split(host_line)
		except:
			print "host line is:", `host_line`
			continue
		bytes = split(ipaddy, '.')
		if int(bytes[0]) <= 2 or int(bytes[0]) >= 224:
			continue
		if int(bytes[0]) in (10, 127):
			continue
		if int(bytes[1]) == 168 and int(bytes[0]) == 192:
			continue
		if int(port) == 0:
			continue
		rawipaddy = join(map(chr, map(int, bytes)), '')
		rawport = pack('H', int(port))
		pongs.append(guid + '\x01\0\0\x0E\0\0\0' + rawport + \
			rawipaddy + '\0'*8)
	f.close()
	return join(pongs, '')

def send_pongs(conn):
	try:
		sent = conn.send(conn.pongs[conn.p:])
	except socket_error, (errno, strerror):
		conn.close()
		conn.flag = CF_NONE
		print "%s  %s:%s %s in send_pongs" % (ctime(time()), \
			conn.addr[0], conn.addr[1], strerror)
		return
	conn.p = conn.p + sent
	if conn.p == len(conn.pongs):
		conn.close()
		conn.flag = CF_NONE
		print "%s  %s:%s pong flood sent" % (ctime(time()), \
			conn.addr[0], conn.addr[1])
		return
	conn.time = int(time())
	return

main()
