'#!/usr/bin/env python
# fileserver.py - File Server v1.0
# Copyright (C) 2002 Michael Leonhard
# www.tamale.net

import socket, select, SocketServer, os, os.path, time, md5
from stat import *

# CHANGE THESE OPTIONS
SERVEDIR = "/home/mike/client/files"	# the directory to serve files from
PORT = 12360				# the TCP port to listen on

class RequestHandler( SocketServer.StreamRequestHandler ):
	def log( self, message ):
		print time.time(), self.client_address, message
		return
	
	def statneg( self, message ):
		# respond to the client
		try: self.wfile.write( "- File unavailable\n" )
		except socket.error: return self.log( "disconnected" )
		return self.log( message )
	
	def stat( self, words ):
		# check that filename exists and there are no directories specified
		(head, tail) = os.path.split( words[1] )
		if len( head ) > 0 or len( tail ) == 0: return self.statneg( "sent invalid filename" )
		
		# make full path
		path = os.path.join( self.server.servedir, tail )
		
		# normalize the path (collapse ./ and ../ entries)
		path = os.path.normpath( path )
		
		# double check directory
		if os.path.dirname( path ) != self.server.servedir: return self.statneg( "!!path mangling: " + string.join( ["%02X"%ord(x) for x in path ] ) + " : " +string.join( ["%02X"%ord(x) for x in words[1] ] ) )
		
		# stat the file
		self.log( "statting file" + path )
		try: finfo = os.stat( path )
		except OSError: return self.statneg( "file does not exist" )
		
		# check file
		if not S_ISREG(finfo[ST_MODE]): return self.statneg( "not a file" )
		
		size = finfo[ST_SIZE]
		modtime = finfo[ST_MTIME]
		
		# open the file
		self.log( "opening" + path )
		try: tochecksum = file( path, "r" )
		except IOError: return self.statneg( "file cannot be read" )

		# generate the checksum
		hash = md5.new()
		while 1:
			# read a block
			data = tochecksum.read( 65536 )
			
			# no more data
			if len( data ) == 0: break
			
			# hash it
			hash.update( data )
		
		# close the file
		tochecksum.close()
		
		# respond to the client
		statstring = "+ %s %s %s\n" % (str( size ), str( modtime ), hash.hexdigest() )
		try: self.wfile.write( statstring )
		except socket.error: return self.log( "disconnected" )
		return self.log( statstring )
	
	def getneg( self, message ):
		# respond to the client
		try: self.wfile.write( "- File unavailable\n" )
		except socket.error: return self.log( "disconnected" )
		return self.log( message )
	
	def get( self, words ):
		# check that two words exist
		if len( words ) != 2: return self.getneg( "malformed message" )
		
		# check that filename exists and there are no directories specified
		(head, tail) = os.path.split( words[1] )
		if len( head ) > 0 or len( tail ) == 0: return self.getneg( "sent invalid filename" )
		
		# make full path
		path = os.path.join( self.server.servedir, tail )
		
		# normalize the path (collapse ./ and ../ entries)
		path = os.path.normpath( path )
		
		# double check directory
		if os.path.dirname( path ) != self.server.servedir: return self.getneg( "!!path mangling: " + string.join( ["%02X"%ord(x) for x in path ] ) + " : " +string.join( ["%02X"%ord(x) for x in words[1] ] ) )
		
		#check that file exists, is regular, and is not a symlink
		if os.path.islink( path ) or not os.path.isfile( path ): return self.log( "requested non-existent file" )
		
		# open the file
		self.log( "opening file" + path )
		try: tosend = file( path, "r" )
		except IOError: return self.log( "asked for file which cannot be read" )
		
		# send the file
		self.log( "sending" )
		while 1:
			# read a block
			data = tosend.read( 65536 )
			
			# no more data
			if len( data ) == 0: break
			
			# send it
			try: self.wfile.write( data )
			except socket.error:
				tosend.close()
				return self.log( "disconnected" )
			#time.sleep( 2 )
		
		return self.log( "sent." + path )
	
	def quit( self ):
		# shutdown the connection properly
		try:
			# disable incoming and outgoing data
			self.request.shutdown( 2 )
			# discard all incoming queued data
			while self.request.recv( 65536 ): pass
			# close the socket
			self.request.close()
		except socket.error: return self.log( "disconnected" )
		
		# all done
		self.log( "quit" )
	
	def handle( self ):
		self.log( "connected" )
		
		# greeting
		try: self.wfile.write( "FILER1 1231047100281\n" )
		except socket.error: return self.log( "disconnected" )
		
		while 1:
			# read the message
			try: data = self.rfile.readline( 128 )
			except socket.error: return self.log( "disconnected" )
			
			# split message into lines
			lines = data.splitlines()
			
			# check that there is only one line
			if len( lines ) != 1: return self.log( "malformed message" )
			
			# split message into two words
			words = lines[0].split( " ", 2 )
			
			# check that there is one word
			if len( words ) < 1 or len( words[0] ) < 1: return self.log( "no keyword" )
			
			# check keyword of message
			if words[0] == "GET": self.get( words )
			elif words[0] == "STAT": self.stat( words )
			elif words[0] == "QUIT": return self.quit()
			else: return self.log( "sent unknown keyword" )

#class ThreadingTCPServer( SocketServer.ThreadingTCPServer ):
#	pass

# the following code is used for debugging
# bind to any port between 12360 and 12370
#for p in range( 12360, 12370 ):
#	try:
#		server = SocketServer.ThreadingTCPServer( ('', p), RequestHandler )
#		print "bound to", p
#		break
#	except socket.error: pass

server = SocketServer.ThreadingTCPServer( ('', PORT), RequestHandler )
server.servedir = SERVEDIR
server.serve_forever()

# end of fileserver.py
