751 lines
		
	
	
		
			24 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
			
		
		
	
	
			751 lines
		
	
	
		
			24 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
| #!/usr/bin/python
 | |
| # -*- coding: utf-8 -*-
 | |
| 
 | |
| # Licensed under GNU General Public License v3 or later
 | |
| # Written by Sebastian Lohff (seba@seba-geek.de)
 | |
| # http://seba-geek.de/stuff/servefile/
 | |
| 
 | |
| __version__ = '0.4.1'
 | |
| 
 | |
| import argparse
 | |
| import base64
 | |
| import cgi
 | |
| import BaseHTTPServer
 | |
| import commands
 | |
| import datetime
 | |
| import urllib
 | |
| import os
 | |
| import re
 | |
| import SimpleHTTPServer
 | |
| import SocketServer
 | |
| import socket
 | |
| from stat import ST_SIZE
 | |
| from subprocess import Popen, PIPE
 | |
| import sys
 | |
| import time
 | |
| 
 | |
| # only activate SSL if available
 | |
| HAVE_SSL = False
 | |
| try:
 | |
| 	from OpenSSL import SSL, crypto
 | |
| 	HAVE_SSL = True
 | |
| except ImportError:
 | |
| 	pass
 | |
| 
 | |
| def getDateStrNow():
 | |
| 	""" Get the current time formatted for HTTP header """
 | |
| 	now = datetime.datetime.fromtimestamp(time.mktime(time.gmtime()))
 | |
| 	return now.strftime("%a, %d %b %Y %H:%M:%S GMT")
 | |
| 
 | |
| class FileBaseHandler(BaseHTTPServer.BaseHTTPRequestHandler):
 | |
| 	fileName = "Undefined"
 | |
| 	blockSize = 1024 * 1024
 | |
| 
 | |
| 	def checkAndDoRedirect(self):
 | |
| 		""" If request didn't request self.fileName redirect to self.fileName.
 | |
| 
 | |
| 		Returns True if a redirect was issued. """
 | |
| 		if urllib.unquote(self.path) != "/" + self.fileName:
 | |
| 			self.send_response(302)
 | |
| 			self.send_header('Location', '/' + self.fileName)
 | |
| 			self.end_headers()
 | |
| 			return True
 | |
| 		return False
 | |
| 
 | |
| class FileHandler(FileBaseHandler):
 | |
| 	filePath = "/dev/null"
 | |
| 	fileLength = 0
 | |
| 	startTime = getDateStrNow()
 | |
| 
 | |
| 	def do_HEAD(self):
 | |
| 		if self.checkAndDoRedirect():
 | |
| 			return
 | |
| 		self.send_response(200)
 | |
| 		self.send_header('Content-Length', self.fileLength)
 | |
| 		self.send_header('Last-Modified', self.startTime)
 | |
| 		self.send_header('Content-Type', 'application/octet-stream')
 | |
| 		self.send_header('Content-Disposition', 'attachment; filename="%s"' % self.fileName)
 | |
| 		self.end_headers()
 | |
| 
 | |
| 	def do_GET(self):
 | |
| 		if self.checkAndDoRedirect():
 | |
| 			return
 | |
| 		myfile = open(self.filePath, 'rb')
 | |
| 
 | |
| 		# find out if this is a continuing download
 | |
| 		fromto = None
 | |
| 		if "Range" in self.headers:
 | |
| 			cont = self.headers.get("Range").split("=")
 | |
| 			if len(cont) > 1 and cont[0] == 'bytes':
 | |
| 				fromto = cont[1].split('-')
 | |
| 				if len(fromto) > 1:
 | |
| 					if fromto[1] == '':
 | |
| 						fromto[1] = self.fileLength-1
 | |
| 					fromto[0] = int(fromto[0])
 | |
| 					fromto[1] = int(fromto[1])
 | |
| 					if fromto[0] >= self.fileLength or fromto[0] < 0 or fromto[1] >= self.fileLength or fromto[1]-fromto[0] < 0:
 | |
| 						# oops, already done!
 | |
| 						self.send_response(416)
 | |
| 						self.send_header('Content-Range', 'bytes */%s' % self.fileLength)
 | |
| 						self.end_headers()
 | |
| 						return
 | |
| 					# now we can wind the file *brrrrrr*
 | |
| 					myfile.seek(fromto[0])
 | |
| 
 | |
| 		if fromto != None:
 | |
| 			self.send_response(216)
 | |
| 			self.send_header('Content-Range', 'bytes %s-%s/%s' % (fromto[0], fromto[1], self.fileLength))
 | |
| 			self.send_header('Content-Length', fromto[1]-fromto[0]+1)
 | |
| 		else:
 | |
| 			self.send_response(200)
 | |
| 			self.send_header('Content-Length', self.fileLength)
 | |
| 		self.send_header('Content-Disposition', 'attachment; filename="%s"' % self.fileName)
 | |
| 		self.send_header('Content-Type', 'application/octet-stream')
 | |
| 		self.send_header('Content-Transfer-Encoding', 'binary')
 | |
| 		self.end_headers()
 | |
| 		block = self.getChunk(myfile, fromto)
 | |
| 		while block:
 | |
| 			try:
 | |
| 				self.wfile.write(block)
 | |
| 			except socket.error, e:
 | |
| 				print "%s ABORTED transmission (Reason %s: %s)" % (self.client_address[0], e[0], e[1])
 | |
| 				return
 | |
| 			block = self.getChunk(myfile, fromto)
 | |
| 		myfile.close()
 | |
| 		print "%s finished downloading" % (self.client_address[0])
 | |
| 		return
 | |
| 
 | |
| 	def getChunk(self, myfile, fromto):
 | |
| 		if fromto and myfile.tell()+self.blockSize >= fromto[1]:
 | |
| 			readsize = fromto[1]-myfile.tell()+1
 | |
| 		else:
 | |
| 			readsize = self.blockSize
 | |
| 		return myfile.read(readsize)
 | |
| 
 | |
| class TarFileHandler(FileBaseHandler):
 | |
| 	target = None
 | |
| 	compression = "none"
 | |
| 	compressionMethods = ("none", "gzip", "bzip2")
 | |
| 
 | |
| 	def do_HEAD(self):
 | |
| 		if self.checkAndDoRedirect():
 | |
| 			return
 | |
| 		self.send_response(200)
 | |
| 		self.send_header('Last-Modified', getDateStrNow())
 | |
| 		self.send_header('Content-Type', 'application/octet-stream')
 | |
| 		self.send_header('Content-Disposition', 'attachment; filename="%s"' % self.fileName)
 | |
| 		self.end_headers()
 | |
| 
 | |
| 	def do_GET(self):
 | |
| 		if self.checkAndDoRedirect():
 | |
| 			return
 | |
| 
 | |
| 		tarCmd = Popen(self.getCompressionCmd(), stdout=PIPE)
 | |
| 		# give the process a short time to find out if it can
 | |
| 		# pack/compress the file
 | |
| 		time.sleep(0.05)
 | |
| 		if tarCmd.poll() != None and tarCmd.poll() != 0:
 | |
| 			# something went wrong
 | |
| 			print "Error while compressing '%s'. Aborting request." % self.target
 | |
| 			self.send_response(500)
 | |
| 			self.end_headers()
 | |
| 			return
 | |
| 
 | |
| 		self.send_response(200)
 | |
| 		self.send_header('Last-Modified', getDateStrNow())
 | |
| 		self.send_header('Content-Type', 'application/octet-stream')
 | |
| 		self.end_headers()
 | |
| 
 | |
| 		block = True
 | |
| 		while block and block != '':
 | |
| 			block = tarCmd.stdout.read(self.blockSize)
 | |
| 			if block and block != '':
 | |
| 				self.wfile.write(block)
 | |
| 		print "%s finished downloading" % (self.client_address[0])
 | |
| 
 | |
| 	def getCompressionCmd(self):
 | |
| 		if self.compression == "none":
 | |
| 			self.fileName += ".tar"
 | |
| 			cmd = ["tar", "-c"]
 | |
| 		elif self.compression == "gzip":
 | |
| 			self.fileName += ".tar.gz"
 | |
| 			cmd = ["tar", "-cz"]
 | |
| 		elif self.compression == "bzip2":
 | |
| 			self.fileName += ".tar.bz2"
 | |
| 			cmd = ["tar", "-cj"]
 | |
| 		else:
 | |
| 			raise ValueError("Unknown compression mode '%s'." % self.compression)
 | |
| 
 | |
| 		dirname = os.path.basename(self.target.rstrip("/"))
 | |
| 		chdirTo = os.path.dirname(self.target.rstrip("/"))
 | |
| 		if chdirTo != '':
 | |
| 			cmd.extend(["-C", chdirTo])
 | |
| 		cmd.append(dirname)
 | |
| 		return cmd
 | |
| 
 | |
| 	@staticmethod
 | |
| 	def getCompressionExt():
 | |
| 		if TarFileHandler.compression == "none":
 | |
| 			return ".tar"
 | |
| 		elif TarFileHandler.compression == "gzip":
 | |
| 			return ".tar.gz"
 | |
| 		elif TarFileHandler.compression == "bzip2":
 | |
| 			return ".tar.bz2"
 | |
| 		raise ValueError("Unknown compression mode '%s'." % self.compression)
 | |
| 
 | |
| class FilePutter(BaseHTTPServer.BaseHTTPRequestHandler):
 | |
| 	""" Simple HTTP Server which allows uploading to a specified directory
 | |
| 	either via multipart/form-data or POST/PUT requests containing the file.
 | |
| 	"""
 | |
| 
 | |
| 	targetDir = None
 | |
| 	maxUploadSize = 0
 | |
| 	uploadPage = """
 | |
| <!docype html>
 | |
| <html>
 | |
| 	<form action="/" method="post" enctype="multipart/form-data">
 | |
| 		<label for="file">Filename:</label>
 | |
| 		<input type="file" name="file" id="file" />
 | |
| 		<br />
 | |
| 		<input type="submit" name="submit" value="Upload" />
 | |
| 	</form>
 | |
| </html>
 | |
| """
 | |
| 
 | |
| 	def do_GET(self):
 | |
| 		""" Answer every GET request with the upload form """
 | |
| 		self.sendResponse(200, self.uploadPage)
 | |
| 
 | |
| 	def do_POST(self):
 | |
| 		""" Upload a file via POST
 | |
| 
 | |
| 		If the content-type is multipart/form-data it checks for the file
 | |
| 		field and saves the data to disk. For other content-types it just
 | |
| 		calls do_PUT and is handled as such except for the http response code.
 | |
| 
 | |
| 		Files can be uploaded with wget --post-file=path/to/file <url> or
 | |
| 		curl -X POST -d @file <url> .
 | |
| 		"""
 | |
| 		length = self.getContentLength()
 | |
| 		if length < 0:
 | |
| 			return
 | |
| 		ctype = self.headers.getheader('Content-Type')
 | |
| 
 | |
| 		# check for multipart/form-data.
 | |
| 		if not (ctype and ctype.lower().startswith("multipart/form-data")):
 | |
| 			# not a normal multipart request ==> handle as PUT request
 | |
| 			return self.do_PUT(fromPost=True)
 | |
| 
 | |
| 		# create FieldStorage object for multipart parsing
 | |
| 		env = os.environ
 | |
| 		env['REQUEST_METHOD'] = "POST"
 | |
| 		fstorage = cgi.FieldStorage(fp=self.rfile, headers=self.headers, environ=env)
 | |
| 		if not "file" in fstorage:
 | |
| 			self.sendResponse(400, "No file found in request.")
 | |
| 			return
 | |
| 
 | |
| 		destFileName = self.getTargetName(fstorage["file"].filename)
 | |
| 		if destFileName == "":
 | |
| 			self.sendResponse(400, "Filename was empty or invalid")
 | |
| 			return
 | |
| 
 | |
| 		# write file down to disk, send an
 | |
| 		target = open(destFileName, "w")
 | |
| 		target.write(fstorage["file"].file.read(length))
 | |
| 		target.close()
 | |
| 		self.sendResponse(200, "OK! Thanks for uploading")
 | |
| 		print "Received file '%s' from %s." % (destFileName, self.client_address[0])
 | |
| 
 | |
| 	def do_PUT(self, fromPost=False):
 | |
| 		""" Upload a file via PUT
 | |
| 
 | |
| 		The request path is used as filename, so uploading a file to the url
 | |
| 		http://host:8080/testfile will cause the file to be named testfile. If
 | |
| 		no filename is given, a random name will be generated.
 | |
| 
 | |
| 		Files can be uploaded with e.g. curl -X POST -d @file <url> .
 | |
| 		"""
 | |
| 		length = self.getContentLength()
 | |
| 		if length < 0:
 | |
| 			return
 | |
| 
 | |
| 		fileName = urllib.unquote(self.path)
 | |
| 		if fileName == "/":
 | |
| 			# if no filename was given we have to generate one
 | |
| 			fileName = str(time.time())
 | |
| 
 | |
| 		cleanFileName = self.getTargetName(fileName)
 | |
| 		if cleanFileName == "":
 | |
| 			self.sendResponse(400, "Filename was invalid")
 | |
| 			return
 | |
| 
 | |
| 		# Sometimes clients want to be told to continue with their transfer
 | |
| 		if self.headers.getheader("Expect") == "100-continue":
 | |
| 			self.send_response(100)
 | |
| 			self.end_headers()
 | |
| 
 | |
| 		target = open(cleanFileName, "w")
 | |
| 		target.write(self.rfile.read(int(self.headers['Content-Length'])))
 | |
| 		target.close()
 | |
| 		self.sendResponse(fromPost and 200 or 201, "OK!")
 | |
| 
 | |
| 	def getContentLength(self):
 | |
| 		length = 0
 | |
| 		try:
 | |
| 			length = int(self.headers['Content-Length'])
 | |
| 		except (ValueError, KeyError):
 | |
| 			pass
 | |
| 		if length <= 0:
 | |
| 			self.sendResponse(411, "Content-Length was invalid or not set.")
 | |
| 			return -1
 | |
| 		if self.maxUploadSize > 0 and length > self.maxUploadSize:
 | |
| 			self.sendResponse(413, "Your file was too big! Maximum allowed size is %d byte. <a href=\"/\">back</a>" % self.maxUploadSize)
 | |
| 			return -1
 | |
| 		return length
 | |
| 
 | |
| 	def sendResponse(self, code, msg):
 | |
| 		""" Send a HTTP response with HTTP statuscode code and message msg,
 | |
| 		providing the correct content-length.
 | |
| 		"""
 | |
| 		self.send_response(code)
 | |
| 		self.send_header('Content-Type', 'text/html')
 | |
| 		self.send_header('Content-Length', str(len(msg)))
 | |
| 		self.end_headers()
 | |
| 		self.wfile.write(msg)
 | |
| 
 | |
| 	def getTargetName(self, fname):
 | |
| 		""" Generate a clean and secure filename.
 | |
| 
 | |
| 		This function takes a filename and strips all the slashes out of it.
 | |
| 		If the file already exists in the target directory, a (NUM) will be
 | |
| 		appended, so no file will be overwritten.
 | |
| 		"""
 | |
| 		cleanFileName = fname.replace("/", "")
 | |
| 		if cleanFileName == "":
 | |
| 			return ""
 | |
| 		destFileName = self.targetDir + "/" + cleanFileName
 | |
| 		if not os.path.exists(destFileName):
 | |
| 			return destFileName
 | |
| 		else:
 | |
| 			i = 1
 | |
| 			extraDestFileName = destFileName + "(%s)" % i
 | |
| 			while os.path.exists(extraDestFileName):
 | |
| 				i += 1
 | |
| 				extraDestFileName = destFileName + "(%s)" % i
 | |
| 			return extraDestFileName
 | |
| 		# never reached
 | |
| 
 | |
| class ThreadedHTTPServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
 | |
| 	pass
 | |
| 
 | |
| def catchSSLErrors(BaseSSLClass):
 | |
| 	""" Class decorator which catches SSL errors and prints them. """
 | |
| 	class X(BaseSSLClass):
 | |
| 		def handle_one_request(self, *args, **kwargs):
 | |
| 			try:
 | |
| 				BaseSSLClass.handle_one_request(self, *args, **kwargs)
 | |
| 			except SSL.Error, e:
 | |
| 				if str(e) == "":
 | |
| 					print "%s SSL Error (Empty error message)" % (self.client_address[0],)
 | |
| 				else:
 | |
| 					print "%s SSL Error: %s" % (self.client_address[0], e)
 | |
| 	return X
 | |
| 
 | |
| class SecureThreadedHTTPServer(ThreadedHTTPServer):
 | |
| 	def __init__(self, pubKey, privKey, *args, **kwargs):
 | |
| 		ThreadedHTTPServer.__init__(self, *args, **kwargs)
 | |
| 		ctx = SSL.Context(SSL.SSLv23_METHOD)
 | |
| 		if type(pubKey) == crypto.X509 and type(privKey) == crypto.PKey:
 | |
| 			ctx.use_certificate(pubKey)
 | |
| 			ctx.use_privatekey(privKey)
 | |
| 		else:
 | |
| 			ctx.use_certificate_file(pubKey)
 | |
| 			ctx.use_privatekey_file(privKey)
 | |
| 
 | |
| 		self.bsocket = socket.socket(self.address_family, self.socket_type)
 | |
| 		self.socket = SSL.Connection(ctx, self.bsocket)
 | |
| 
 | |
| 		self.server_bind()
 | |
| 		self.server_activate()
 | |
| 
 | |
| 	def shutdown_request(self, request):
 | |
| 		request.shutdown()
 | |
| 
 | |
| class SecureHandler():
 | |
| 	def setup(self):
 | |
| 		self.connection = self.request
 | |
| 		self.rfile = socket._fileobject(self.request, "rb", self.rbufsize)
 | |
| 		self.wfile = socket._fileobject(self.request, "wb", self.wbufsize)
 | |
| 
 | |
| class ServeFileException(Exception):
 | |
| 	pass
 | |
| 
 | |
| class ServeFile():
 | |
| 	""" Main class to manage everything. """
 | |
| 
 | |
| 	_NUM_MODES = 4
 | |
| 	(MODE_SINGLE, MODE_SINGLETAR, MODE_UPLOAD, MODE_LISTDIR) = range(_NUM_MODES)
 | |
| 
 | |
| 	def __init__(self, target, port=8080, serveMode=0, useSSL=False):
 | |
| 		self.target = target
 | |
| 		self.port = port
 | |
| 		self.serveMode = serveMode
 | |
| 		self.dirCreated = False
 | |
| 		self.useSSL = useSSL
 | |
| 		self.cert = self.key = None
 | |
| 		self.auth = None
 | |
| 		self.maxUploadSize = 0
 | |
| 
 | |
| 		if self.serveMode not in range(self._NUM_MODES):
 | |
| 			self.serveMode = None
 | |
| 			raise ValueError("Unknown serve mode, needs to be MODE_SINGLE, MODE_SINGLETAR, MODE_UPLOAD or MODE_DIRLIST.")
 | |
| 
 | |
| 	def getIPs(self):
 | |
| 		""" Get IPs from all interfaces via ip or ifconfig. """
 | |
| 		# ip and ifconfig sometimes are located in /sbin/
 | |
| 		os.environ['PATH'] += ':/sbin:/usr/sbin'
 | |
| 		proc = Popen(r"ip addr|" + \
 | |
| 					  "sed -n -e 's/.*inet6\? \([0-9.a-fA-F:]\+\)\/.*/\\1/ p'|" + \
 | |
| 					  "grep -v '^fe80\|^127.0.0.1\|^::1'", \
 | |
| 					  shell=True, stdout=PIPE, stderr=PIPE)
 | |
| 		if proc.wait() != 0:
 | |
| 			# ip failed somehow, falling back to ifconfig
 | |
| 			oldLang = os.environ.get("LC_ALL", None)
 | |
| 			os.environ['LC_ALL'] = "C"
 | |
| 			proc = Popen(r"ifconfig|" + \
 | |
| 						  "sed -n 's/.*inet6\? addr: \?\([0-9a-fA-F.:]*\).*/" + \
 | |
| 						  "\\1/p'|" + \
 | |
| 						  "grep -v '^fe80\|^127.0.0.1\|^::1'", \
 | |
| 						  shell=True, stdout=PIPE, stderr=PIPE)
 | |
| 			if oldLang:
 | |
| 				os.environ['LC_ALL'] = oldLang
 | |
| 			else:
 | |
| 				del(os.environ['LC_ALL'])
 | |
| 			if proc.wait() != 0:
 | |
| 				# we couldn't find any ip address
 | |
| 				proc = None
 | |
| 		if proc:
 | |
| 			ips = proc.stdout.read().strip().split("\n")
 | |
| 			# FIXME: When BaseHTTP supports ipv6 properly, delete this line
 | |
| 			ips = filter(lambda ip: ip.find(":") == -1, ips)
 | |
| 			return ips
 | |
| 		return None
 | |
| 
 | |
| 	def setSSLKeys(self, cert, key):
 | |
| 		""" Set SSL cert/key. Can be either path to file or pyssl X509/PKey object. """
 | |
| 		self.cert = cert
 | |
| 		self.key = key
 | |
| 
 | |
| 	def setMaxUploadSize(self, limit):
 | |
| 		""" Set the maximum upload size in byte """
 | |
| 		self.maxUploadSize = limit
 | |
| 
 | |
| 	def setCompression(self, compression):
 | |
| 		""" Set the compression of TarFileHandler """
 | |
| 		if self.serveMode != self.MODE_SINGLETAR:
 | |
| 			raise ServeFileException("Compression mode can only be set in tar-mode.")
 | |
| 		if compression not in TarFileHandler.compressionMethods:
 | |
| 			raise ServeFileException("Compression mode not available.")
 | |
| 		TarFileHandler.compression = compression
 | |
| 
 | |
| 	def genKeyPair(self):
 | |
| 		print "Generating SSL certificate...",
 | |
| 		sys.stdout.flush()
 | |
| 
 | |
| 		pkey = crypto.PKey()
 | |
| 		pkey.generate_key(crypto.TYPE_RSA, 2048)
 | |
| 
 | |
| 		req = crypto.X509Req()
 | |
| 		subj = req.get_subject()
 | |
| 		subj.CN = "127.0.0.1"
 | |
| 		subj.O = "servefile laboratories"
 | |
| 		subj.OU = "servefile"
 | |
| 
 | |
| 		# generate altnames
 | |
| 		altNames = []
 | |
| 		for ip in self.getIPs() + ["127.0.0.1"]:
 | |
| 			altNames.append("IP:%s" % ip)
 | |
| 		altNames.append("DNS:localhost")
 | |
| 		ext = crypto.X509Extension("subjectAltName", False, ",".join(altNames))
 | |
| 		req.add_extensions([ext])
 | |
| 
 | |
| 		req.set_pubkey(pkey)
 | |
| 		req.sign(pkey, "sha1")
 | |
| 
 | |
| 		cert = crypto.X509()
 | |
| 		# some browsers complain if they see a cert from the same authority
 | |
| 		# with the same serial ==> we just use the seconds as serial.
 | |
| 		cert.set_serial_number(int(time.time()))
 | |
| 		cert.gmtime_adj_notBefore(0)
 | |
| 		cert.gmtime_adj_notAfter(365*24*60*60)
 | |
| 		cert.set_issuer(req.get_subject())
 | |
| 		cert.set_subject(req.get_subject())
 | |
| 		cert.add_extensions([ext])
 | |
| 		cert.set_pubkey(req.get_pubkey())
 | |
| 		cert.sign(pkey, "sha1")
 | |
| 
 | |
| 		self.cert = cert
 | |
| 		self.key = pkey
 | |
| 
 | |
| 		print "done."
 | |
| 		print "SHA1 fingerprint:", cert.digest("sha1")
 | |
| 		print "MD5  fingerprint:", cert.digest("md5")
 | |
| 
 | |
| 	def _getCert(self):
 | |
| 		return self.cert
 | |
| 
 | |
| 	def _getKey(self):
 | |
| 		return self.key
 | |
| 
 | |
| 	def setAuth(self, user, password):
 | |
| 		if len(user) == "" or len(password) == "":
 | |
| 			raise ServeFileException("User and password both need to be at least one character.")
 | |
| 		self.auth = base64.b64encode("%s:%s" % (user, password))
 | |
| 
 | |
| 	def _createServer(self, handler):
 | |
| 		server = None
 | |
| 		if self.useSSL:
 | |
| 			if not self._getKey():
 | |
| 				self.genKeyPair()
 | |
| 			server = SecureThreadedHTTPServer(self._getCert(), self._getKey(), ('', self.port), handler)
 | |
| 		else:
 | |
| 			server = ThreadedHTTPServer(('', self.port), handler)
 | |
| 		return server
 | |
| 
 | |
| 	def serve(self):
 | |
| 		self.handler = self._confAndFindHandler()
 | |
| 		self.server = self._createServer(self.handler)
 | |
| 
 | |
| 		if self.serveMode != self.MODE_UPLOAD:
 | |
| 			print "Serving \"%s\" at port %d." % (self.target, self.port)
 | |
| 		else:
 | |
| 			print "Serving \"%s\" for uploads at port %d." % (self.target, self.port)
 | |
| 
 | |
| 		# print urls with local network adresses
 | |
| 		print "\nSome addresses %s will be available at:" % \
 | |
| 				((self.serveMode != self.MODE_UPLOAD) and "this file" or "the uploadform", )
 | |
| 		ips = self.getIPs()
 | |
| 		if not ips or len(ips) == 0 or ips[0] == '':
 | |
| 			print "Could not find any addresses."
 | |
| 		else:
 | |
| 			for ip in ips:
 | |
| 				print "\thttp%s://%s:%d/" % (self.useSSL and "s" or "", ip, self.port)
 | |
| 		print ""
 | |
| 
 | |
| 		try:
 | |
| 			self.server.serve_forever()
 | |
| 		except KeyboardInterrupt:
 | |
| 			self.server.socket.close()
 | |
| 
 | |
| 		# cleanup potential upload directory
 | |
| 		if self.dirCreated and len(os.listdir(self.target)) == 0:
 | |
| 			# created upload dir was not used
 | |
| 			os.rmdir(self.target)
 | |
| 
 | |
| 	def _confAndFindHandler(self):
 | |
| 		handler = None
 | |
| 		if self.serveMode == self.MODE_SINGLE:
 | |
| 			try:
 | |
| 				testit = open(self.target, 'r')
 | |
| 				testit.close()
 | |
| 				FileHandler.filePath = self.target
 | |
| 				FileHandler.fileName = os.path.basename(self.target)
 | |
| 				FileHandler.fileLength = os.stat(self.target)[ST_SIZE]
 | |
| 			except IOError:
 | |
| 				raise ServeFileException("Error: Could not open file!")
 | |
| 			handler = FileHandler
 | |
| 		elif self.serveMode == self.MODE_SINGLETAR:
 | |
| 			self.realTarget = os.path.realpath(self.target)
 | |
| 			if not os.path.exists(self.realTarget):
 | |
| 				raise ServeFileException("Error: Could not open file or directory.")
 | |
| 			TarFileHandler.target = self.realTarget
 | |
| 			TarFileHandler.fileName = os.path.basename(self.realTarget.rstrip("/")) + TarFileHandler.getCompressionExt()
 | |
| 
 | |
| 			handler = TarFileHandler
 | |
| 		elif self.serveMode == self.MODE_UPLOAD:
 | |
| 			if os.path.isdir(self.target):
 | |
| 				print "Warning: Uploading to an already existing directory."
 | |
| 			elif not os.path.exists(self.target):
 | |
| 				self.dirCreated = True
 | |
| 				try:
 | |
| 					os.mkdir(self.target)
 | |
| 				except IOError, OSError:
 | |
| 					raise ServeFileException("Error: Could not create directory '%s' for uploads." % (self.target,) )
 | |
| 			else:
 | |
| 				raise ServeFileException("Error: Upload directory already exists and is a file.")
 | |
| 			FilePutter.targetDir = self.target
 | |
| 			FilePutter.maxUploadSize = self.maxUploadSize
 | |
| 			handler = FilePutter
 | |
| 		elif self.serveMode == self.MODE_LISTDIR:
 | |
| 			try:
 | |
| 				os.chdir(self.target)
 | |
| 			except OSError:
 | |
| 				raise ServeFileException("Error: Could not change directory to '%s'." % self.target)
 | |
| 			handler = SimpleHTTPServer.SimpleHTTPRequestHandler
 | |
| 
 | |
| 
 | |
| 		if self.auth:
 | |
| 			# do authentication
 | |
| 			AuthenticationHandler.authString = self.auth
 | |
| 			class AuthenticatedHandler(AuthenticationHandler, handler):
 | |
| 				pass
 | |
| 			handler = AuthenticatedHandler
 | |
| 
 | |
| 		if self.useSSL:
 | |
| 			# secure handler
 | |
| 			@catchSSLErrors
 | |
| 			class AlreadySecuredHandler(SecureHandler, handler):
 | |
| 				pass
 | |
| 			handler = AlreadySecuredHandler
 | |
| 		return handler
 | |
| 
 | |
| class AuthenticationHandler():
 | |
| 	# base64 encoded user:password string for authentication
 | |
| 	authString = None
 | |
| 	realm = "Restricted area"
 | |
| 
 | |
| 	def handle_one_request(self):
 | |
| 		""" Overloaded function to handle one request.
 | |
| 
 | |
| 		Before calling the responsible do_METHOD function, check credentials
 | |
| 		"""
 | |
| 		self.raw_requestline = self.rfile.readline()
 | |
| 		if not self.raw_requestline:
 | |
| 			self.close_connection = 1
 | |
| 			return
 | |
| 		if not self.parse_request(): # An error code has been sent, just exit
 | |
| 			return
 | |
| 
 | |
| 		authorized = False
 | |
| 		if "Authorization" in self.headers:
 | |
| 			if self.headers["Authorization"] == ("Basic " + self.authString):
 | |
| 				authorized = True
 | |
| 		if authorized:
 | |
| 			mname = 'do_' + self.command
 | |
| 			if not hasattr(self, mname):
 | |
| 				self.send_error(501, "Unsupported method (%r)" % self.command)
 | |
| 				return
 | |
| 			method = getattr(self, mname)
 | |
| 			method()
 | |
| 		else:
 | |
| 			self.send_response(401)
 | |
| 			self.send_header("WWW-Authenticate", "Basic realm=\"%s\"" % self.realm)
 | |
| 
 | |
| def main():
 | |
| 	parser = argparse.ArgumentParser(description='Serve a single file via HTTP.')
 | |
| 	parser.add_argument('--version', action='version', version='%(prog)s ' + __version__)
 | |
| 	parser.add_argument('target', metavar='file/directory', type=str)
 | |
| 	parser.add_argument('-p', '--port', type=int, default=8080, \
 | |
| 	                    help='Port to listen on')
 | |
| 	parser.add_argument('-u', '--upload', action="store_true", default=False, \
 | |
| 	                    help="Enable uploads to a given directory")
 | |
| 	parser.add_argument('-s', '--max-upload-size', type=str, \
 | |
| 	                    help="Limit upload size in kB. Size modifiers are allowed, e.g. 2G, 12MB, 1B")
 | |
| 	parser.add_argument('-l', '--list-dir', action="store_true", default=False, \
 | |
| 	                    help="Show directory indexes and allow access to all subdirectories")
 | |
| 	parser.add_argument('--ssl', action="store_true", default=False, \
 | |
| 	                    help="Enable SSL. If no key/cert is specified one will be generated")
 | |
| 	parser.add_argument('--key', type=str, \
 | |
| 	                    help="Keyfile to use for SSL. If no cert is given with --cert the keyfile will also be searched for a cert")
 | |
| 	parser.add_argument('--cert', type=str, \
 | |
| 	                    help="Certfile to use for SSL")
 | |
| 	parser.add_argument('-a', '--auth', type=str, metavar='user:password', \
 | |
| 	                    help="Set user and password for HTTP basic authentication")
 | |
| 	parser.add_argument('-t', '--tar', action="store_true", default=False, \
 | |
| 	                    help="Enable on the fly tar creation for given file or directory. Note: Download continuation will not be available")
 | |
| 	parser.add_argument('-c', '--compression', type=str, metavar='method', \
 | |
| 	                    default="none", \
 | |
| 	                    help="Set compression method, only in combination with --tar. Can be one of %s" % ", ".join(TarFileHandler.compressionMethods))
 | |
| 
 | |
| 	args = parser.parse_args()
 | |
| 	maxUploadSize = 0
 | |
| 
 | |
| 	# check for invalid option combinations/preparse stuff
 | |
| 	if args.max_upload_size and not args.upload:
 | |
| 		print "Error: Maximum upload size can only be specified when in upload mode."
 | |
| 		sys.exit(1)
 | |
| 
 | |
| 	if args.max_upload_size:
 | |
| 		sizeRe = re.match("^(\d+(?:[,.]\d+)?)(?:([bkmgtpe])(?:(?<!b)b?)?)?$", args.max_upload_size.lower())
 | |
| 		if not sizeRe:
 | |
| 			print "Error: Your max upload size param is broken."
 | |
| 			sys.exit(1)
 | |
| 		uploadSize, modifier = sizeRe.groups()
 | |
| 		uploadSize = float(uploadSize.replace(",", "."))
 | |
| 		sizes = ["b", "k", "m", "g", "t", "p", "e"]
 | |
| 		maxUploadSize = int(uploadSize * pow(1024, sizes.index(modifier or "k")))
 | |
| 		if maxUploadSize < 0:
 | |
| 			print "Error: Your max upload size can't be negative"
 | |
| 			sys.exit(1)
 | |
| 
 | |
| 	if args.ssl and not HAVE_SSL:
 | |
| 		print "Error: SSL is not available, please install pyssl (python-openssl)."
 | |
| 		sys.exit(1)
 | |
| 
 | |
| 	if args.cert and not args.key:
 | |
| 		print "Error: Please specify a key along with your cert."
 | |
| 		sys.exit(1)
 | |
| 
 | |
| 	if not args.ssl and (args.cert or args.key):
 | |
| 		print "Error: You need to enable ssl with --ssl when specifying certs/keys."
 | |
| 		sys.exit(1)
 | |
| 
 | |
| 	if args.auth:
 | |
| 		dpos = args.auth.find(":")
 | |
| 		if dpos <= 0 or dpos == (len(args.auth)-1):
 | |
| 			print "Error: User and password for HTTP basic authentication need to be both at least one character band have to be seperated by a \":\"."
 | |
| 			sys.exit(1)
 | |
| 
 | |
| 	if args.compression != "none" and not args.tar:
 | |
| 		print "Error: Please use --tar if you want to tar everything."
 | |
| 		sys.exit(1)
 | |
| 
 | |
| 	if args.tar and args.upload:
 | |
| 		print "Error: --tar mode will not work with uploads."
 | |
| 		sys.exit(1)
 | |
| 
 | |
| 	if args.tar and args.list_dir:
 | |
| 		print "Error: --tar mode will not work with directory listings."
 | |
| 		sys.exit(1)
 | |
| 
 | |
| 	compression = None
 | |
| 	if args.compression:
 | |
| 		if args.compression in TarFileHandler.compressionMethods:
 | |
| 			compression = args.compression
 | |
| 		else:
 | |
| 			print "Error: Compression mode '%s' is unknown." % self.compression
 | |
| 			sys.exit(1)
 | |
| 
 | |
| 	mode = None
 | |
| 	if args.upload:
 | |
| 		mode = ServeFile.MODE_UPLOAD
 | |
| 	elif args.list_dir:
 | |
| 		mode = ServeFile.MODE_LISTDIR
 | |
| 	elif args.tar:
 | |
| 		mode = ServeFile.MODE_SINGLETAR
 | |
| 	else:
 | |
| 		mode = ServeFile.MODE_SINGLE
 | |
| 
 | |
| 	server = None
 | |
| 	try:
 | |
| 		server = ServeFile(args.target, args.port, mode, args.ssl)
 | |
| 		if maxUploadSize > 0:
 | |
| 			server.setMaxUploadSize(maxUploadSize)
 | |
| 		if args.ssl and args.key:
 | |
| 			cert = args.cert or args.key
 | |
| 			server.setSSLKeys(cert, args.key)
 | |
| 		if args.auth:
 | |
| 			user, password = args.auth.split(":", 1)
 | |
| 			server.setAuth(user, password)
 | |
| 		if compression and compression != "none":
 | |
| 			server.setCompression(compression)
 | |
| 		server.serve()
 | |
| 	except ServeFileException, e:
 | |
| 		print e
 | |
| 		sys.exit(1)
 | |
| 	print "Good bye."
 | |
| 
 | |
| if __name__ == '__main__':
 | |
| 	main()
 | |
| 
 |