SebaGeek Lectus non solum ad dormiendum factus est.

Scripte & Zeug: Python Servefile Script

Das ist ein kleines Script, welches ich gebastelt habe um schnell ein paar Dateien im LAN zu verschicken. Startet man das Script, so öffnet es einen threaded Webserver auf Port 8080 und erlaubt es anderen diese Datei über HTTP herunterzuladen (der angegebene Pfad wird dabei ignoriert).

Download:

Git (development version) git clone git://git.someserver.de/git/servefile/
Source-Tarball servefile-0.4.2.tar.gz
Debian Repository servefile
Gentoo www-servers/servefile
Arch AUR/servefile

Old tarballs servefile-0.4.1.tar.gz
servefile-0.4.0.tar.gz
servefile-0.3.2.tar.gz

   1 #!/usr/bin/python
   2 # -*- coding: utf-8 -*-
   3 
   4 # Licensed under GNU General Public License v3 or later
   5 # Written by Sebastian Lohff (seba@seba-geek.de)
   6 # http://seba-geek.de/stuff/servefile/
   7 
   8 from __future__ import print_function
   9 
  10 __version__ = '0.4.2'
  11 
  12 import argparse
  13 import base64
  14 import cgi
  15 import datetime
  16 import mimetypes
  17 import urllib
  18 import os
  19 import posixpath
  20 import re
  21 import select
  22 import socket
  23 from subprocess import Popen, PIPE
  24 import sys
  25 import time
  26 
  27 # fix imports for python2/python3
  28 try:
  29     import BaseHTTPServer
  30     import SocketServer
  31 except ImportError:
  32     # both have different names in python3
  33     import http.server as BaseHTTPServer
  34     import socketserver as SocketServer
  35 
  36 # only activate SSL if available
  37 HAVE_SSL = False
  38 try:
  39 	from OpenSSL import SSL, crypto
  40 	HAVE_SSL = True
  41 except ImportError:
  42 	pass
  43 
  44 def getDateStrNow():
  45 	""" Get the current time formatted for HTTP header """
  46 	now = datetime.datetime.fromtimestamp(time.mktime(time.gmtime()))
  47 	return now.strftime("%a, %d %b %Y %H:%M:%S GMT")
  48 
  49 class FileBaseHandler(BaseHTTPServer.BaseHTTPRequestHandler):
  50 	fileName = None
  51 	blockSize = 1024 * 1024
  52 	server_version = "servefile/" + __version__
  53 
  54 	def checkAndDoRedirect(self, fileName=None):
  55 		""" If request didn't request self.fileName redirect to self.fileName.
  56 
  57 		Returns True if a redirect was issued. """
  58 		if not fileName:
  59 			fileName = self.fileName
  60 		if urllib.unquote(self.path) != "/" + fileName:
  61 			self.send_response(302)
  62 			self.send_header('Location', '/' + fileName)
  63 			self.end_headers()
  64 			return True
  65 		return False
  66 
  67 	def sendContentHeaders(self, fileName, fileLength, lastModified=None):
  68 		""" Send default Content headers for given fileName and fileLength.
  69 
  70 		If no lastModified is given the current date is taken. If
  71 		fileLength is lesser than 0 no Content-Length will be sent."""
  72 		if not lastModified:
  73 			lastModified = getDateStrNow()
  74 
  75 		if fileLength >= 0:
  76 			self.send_header('Content-Length', str(fileLength))
  77 		self.send_header('Last-Modified', lastModified)
  78 		self.send_header('Content-Type', 'application/octet-stream')
  79 		self.send_header('Content-Disposition', 'attachment; filename="%s"' % fileName)
  80 		self.send_header('Content-Transfer-Encoding', 'binary')
  81 
  82 	def isRangeRequest(self):
  83 		""" Return True if partial content is requestet """
  84 		return "Range" in self.headers
  85 
  86 	def handleRangeRequest(self, fileLength):
  87 		""" Find out and handle continuing downloads.
  88 
  89 		Returns a tuple of a boolean, if this is a valid range request,
  90 		and a range. When the requested range is out of range, range is
  91 		set to None.
  92 		"""
  93 		fromto = None
  94 		if self.isRangeRequest():
  95 			cont = self.headers.get("Range").split("=")
  96 			if len(cont) > 1 and cont[0] == 'bytes':
  97 				fromto = cont[1].split('-')
  98 				if len(fromto) > 1:
  99 					if fromto[1] == '':
 100 						fromto[1] = fileLength - 1
 101 					try:
 102 						fromto[0] = int(fromto[0])
 103 						fromto[1] = int(fromto[1])
 104 					except:
 105 						return (False, None)
 106 
 107 					if fromto[0] >= fileLength or fromto[0] < 0 or fromto[1] >= fileLength or fromto[1]-fromto[0] < 0:
 108 						# oops, already done! (requested range out of range)
 109 						self.send_response(416)
 110 						self.send_header('Content-Range', 'bytes */%d' % fileLength)
 111 						self.end_headers()
 112 						return (True, None)
 113 					return (True, fromto)
 114 		# broken request or no range header
 115 		return (False, None)
 116 
 117 	def sendFile(self, filePath, fileLength=None, lastModified=None):
 118 		""" Send file with continuation support.
 119 
 120 		filePath: path to file to be sent
 121 		fileLength: length of file (if None is given this will be found out)
 122 		lastModified: time the file was last modified, None for "now"
 123 		"""
 124 		if not fileLength:
 125 			fileLength = os.stat(filePath).st_size
 126 
 127 		(responseCode, myfile) = self.getFileHandle(filePath)
 128 		if not myfile:
 129 			self.send_response(responseCode)
 130 			self.end_headers()
 131 			return
 132 
 133 		(continueDownload, fromto) = self.handleRangeRequest(fileLength)
 134 		if continueDownload:
 135 			if not fromto:
 136 				# we are done
 137 				return True
 138 
 139 			# now we can wind the file *brrrrrr*
 140 			myfile.seek(fromto[0])
 141 
 142 		if fromto != None:
 143 			self.send_response(216)
 144 			self.send_header('Content-Range', 'bytes %d-%d/%d' % (fromto[0], fromto[1], fileLength))
 145 			fileLength = fromto[1] - fromto[0] + 1
 146 		else:
 147 			self.send_response(200)
 148 
 149 		fileName = self.fileName
 150 		if not fileName:
 151 			fileName = os.path.basename(filePath)
 152 		self.sendContentHeaders(fileName, fileLength, lastModified)
 153 		self.end_headers()
 154 		block = self.getChunk(myfile, fromto)
 155 		while block:
 156 			try:
 157 				self.wfile.write(block)
 158 			except socket.error as e:
 159 				print("%s ABORTED transmission (Reason %s: %s)" % (self.client_address[0], e[0], e[1]))
 160 				return False
 161 			block = self.getChunk(myfile, fromto)
 162 		myfile.close()
 163 		print("%s finished downloading %s" % (self.client_address[0], filePath))
 164 		return True
 165 
 166 	def getChunk(self, myfile, fromto):
 167 		if fromto and myfile.tell()+self.blockSize >= fromto[1]:
 168 			readsize = fromto[1]-myfile.tell()+1
 169 		else:
 170 			readsize = self.blockSize
 171 		return myfile.read(readsize)
 172 
 173 	def getFileHandle(self, path):
 174 		""" Get handle to a file.
 175 
 176 		Return a tuple of HTTP response code and file handle.
 177 		If the handle couldn't be acquired it is set to None
 178 		and an appropriate HTTP error code is returned.
 179 		"""
 180 		myfile = None
 181 		responseCode = 200
 182 		try:
 183 			myfile = open(path, 'rb')
 184 		except IOError as e:
 185 			responseCode = self.getResponseForErrno(e.errno)
 186 		return (responseCode, myfile)
 187 
 188 	def getFileLength(self, path):
 189 		""" Get length of a file.
 190 
 191 		Return a tuple of HTTP response code and file length.
 192 		If filelength couldn't be determined, it is set to -1
 193 		and an appropriate HTTP error code is returned.
 194 		"""
 195 		fileSize = -1
 196 		responseCode = 200
 197 		try:
 198 			fileSize = os.stat(path).st_size
 199 		except IOError as e:
 200 			responseCode = self.getResponseForErrno(e.errno)
 201 		return (responseCode, fileSize)
 202 
 203 	def getResponseForErrno(self, errno):
 204 		""" Return HTTP response code for an IOError errno """
 205 		if errno == errno.ENOENT:
 206 			return 404
 207 		elif errno == errno.EACCESS:
 208 			return 403
 209 		else:
 210 			return 500
 211 
 212 
 213 class FileHandler(FileBaseHandler):
 214 	filePath = "/dev/null"
 215 	fileLength = 0
 216 	startTime = getDateStrNow()
 217 
 218 	def do_HEAD(self):
 219 		if self.checkAndDoRedirect():
 220 			return
 221 		self.send_response(200)
 222 		self.sendContentHeaders(self.fileName, self.fileLength, self.startTime)
 223 		self.end_headers()
 224 
 225 	def do_GET(self):
 226 		if self.checkAndDoRedirect():
 227 			return
 228 		self.sendFile(self.filePath, self.fileLength, self.startTime)
 229 
 230 
 231 class TarFileHandler(FileBaseHandler):
 232 	target = None
 233 	compression = "none"
 234 	compressionMethods = ("none", "gzip", "bzip2")
 235 
 236 	def do_HEAD(self):
 237 		if self.checkAndDoRedirect():
 238 			return
 239 		self.send_response(200)
 240 		self.sendContentHeaders(self.fileName, -1)
 241 		self.end_headers()
 242 
 243 	def do_GET(self):
 244 		if self.checkAndDoRedirect():
 245 			return
 246 
 247 		tarCmd = Popen(self.getCompressionCmd(), stdout=PIPE)
 248 		# give the process a short time to find out if it can
 249 		# pack/compress the file
 250 		time.sleep(0.05)
 251 		if tarCmd.poll() != None and tarCmd.poll() != 0:
 252 			# something went wrong
 253 			print("Error while compressing '%s'. Aborting request." % self.target)
 254 			self.send_response(500)
 255 			self.end_headers()
 256 			return
 257 
 258 		self.send_response(200)
 259 		self.sendContentHeaders(self.fileName, -1)
 260 		self.end_headers()
 261 
 262 		block = True
 263 		while block and block != '':
 264 			block = tarCmd.stdout.read(self.blockSize)
 265 			if block and block != '':
 266 				self.wfile.write(block)
 267 		print("%s finished downloading" % (self.client_address[0]))
 268 
 269 	def getCompressionCmd(self):
 270 		if self.compression == "none":
 271 			cmd = ["tar", "-c"]
 272 		elif self.compression == "gzip":
 273 			cmd = ["tar", "-cz"]
 274 		elif self.compression == "bzip2":
 275 			cmd = ["tar", "-cj"]
 276 		else:
 277 			raise ValueError("Unknown compression mode '%s'." % self.compression)
 278 
 279 		dirname = os.path.basename(self.target.rstrip("/"))
 280 		chdirTo = os.path.dirname(self.target.rstrip("/"))
 281 		if chdirTo != '':
 282 			cmd.extend(["-C", chdirTo])
 283 		cmd.append(dirname)
 284 		return cmd
 285 
 286 	@staticmethod
 287 	def getCompressionExt():
 288 		if TarFileHandler.compression == "none":
 289 			return ".tar"
 290 		elif TarFileHandler.compression == "gzip":
 291 			return ".tar.gz"
 292 		elif TarFileHandler.compression == "bzip2":
 293 			return ".tar.bz2"
 294 		raise ValueError("Unknown compression mode '%s'." % TarFileHandler.compression)
 295 
 296 
 297 class DirListingHandler(FileBaseHandler):
 298 	""" DOCUMENTATION MISSING """
 299 
 300 	targetDir = None
 301 
 302 	def do_HEAD(self):
 303 		self.getFileOrDirectory(head=True)
 304 
 305 	def do_GET(self):
 306 		self.getFileOrDirectory(head=False)
 307 
 308 	def getFileOrDirectory(self, head=False):
 309 		""" Send file or directory index, depending on requested path """
 310 		path = self.getCleanPath()
 311 
 312 		if os.path.isdir(path):
 313 			if not self.path.endswith('/'):
 314 				self.send_response(301)
 315 				self.send_header("Location", self.path + '/')
 316 				self.end_headers()
 317 			else:
 318 				self.sendDirectoryListing(path, head)
 319 		elif os.path.isfile(path):
 320 			if head:
 321 				(response, length) = self.getFileLength(path)
 322 				if length < 0:
 323 					self.send_response(response)
 324 					self.end_headers()
 325 				else:
 326 					self.send_response(200)
 327 					self.sendContentHeaders(self, path, length)
 328 					self.end_headers()
 329 			else:
 330 				self.sendFile(path, head)
 331 		else:
 332 			self.send_response(404)
 333 			errorMsg = """<!DOCTYPE html><html>
 334 				<head><title>404 Not Found</title></head>
 335 				<body>
 336 				<h1>Not Found</h1>
 337 				<p>The requestet URL %s was not found on this server</p>
 338 				<p><a href="/">Back to /</a>
 339 				</body>
 340 				</html>""" % self.escapeHTML(urllib.unquote(self.path))
 341 			self.send_header("Content-Length", str(len(errorMsg)))
 342 			self.end_headers()
 343 			if not head:
 344 				self.wfile.write(errorMsg)
 345 
 346 	def escapeHTML(self, htmlstr):
 347 		entities = [("<", "&lt;"), (">", "&gt;")]
 348 		for src, dst in entities:
 349 			htmlstr = htmlstr.replace(src, dst)
 350 		return htmlstr
 351 
 352 	def sendDirectoryListing(self, path, head):
 353 		""" Generate a directorylisting for path and send it """
 354 		header = """<!DOCTYPE html>
 355 <html>
 356 	<head>
 357 		<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
 358 		<title>Index of %(path)s</title>
 359 		<style type="text/css">
 360 			a { text-decoration: none; color: #0000BB;}
 361 			a:visited { color: #000066;}
 362 			a:hover, a:focus, a:active { text-decoration: underline; color: #cc0000; text-indent: 5px; }
 363 			body { background-color: #eaeaea; padding: 20px 0; margin: 0; font: 400 13px/1.2em Arial, sans-serif; }
 364 			h1 { margin: 0 10px 12px 10px; font-family: Arial, sans-serif; }
 365 			div.content { background-color: white; border-color: #ccc; border-width: 1px 0; border-style: solid; padding: 10px 10px 15px 10px; }
 366 			td { padding-right: 15px; text-align: left; font-family: monospace; }
 367 			th { font-weight: bold; font-size: 115%%; padding: 0 15px 5px 0; text-align: left; }
 368 			.size { text-align: right; }
 369 			.footer { font: 12px monospace; color: #333; margin: 5px 10px 0; }
 370 			.footer, h1 { text-shadow: 0 1px 0 white; }
 371 		</style>
 372 	</head>
 373 <body>
 374 	<h1>Index of %(path)s</h1>
 375 	<div class="content">
 376 	<table summary="Directory Listing">
 377 		<thead>
 378 			<tr>
 379 				<th class="name">Name</th>
 380 				<th class="lastModified">Last Modified</th>
 381 				<th class="size">Size</th>
 382 				<th class="type">Type</th>
 383 			</tr>
 384 		</thead>
 385 		<tbody>
 386 		""" % {'path': posixpath.normpath(urllib.unquote(self.path))}
 387 		footer = """</tbody></table></div>
 388 <div class="footer"><a href="http://seba-geek.de/stuff/servefile/">servefile %(version)s</a></div>
 389 </body>
 390 </html>""" % {'version': __version__}
 391 		content = []
 392 		for item in [".."] + sorted(os.listdir(path)):
 393 				# create path to item
 394 				itemPath = os.path.join(path, item)
 395 
 396 				# try to stat file for size, last modified... continue on error
 397 				stat = None
 398 				try:
 399 					stat = os.stat(itemPath)
 400 				except IOError:
 401 					continue
 402 
 403 				# Strings to display on directory listing
 404 				lastModifiedDate = datetime.datetime.fromtimestamp(stat.st_mtime)
 405 				lastModified = lastModifiedDate.strftime("%Y-%m-%d %H:%M")
 406 				fileSize = "%.1f%s" % self.convertSize(stat.st_size)
 407 				(fileType, _) = mimetypes.guess_type(itemPath)
 408 				if not fileType:
 409 					fileType = "-"
 410 
 411 				if os.path.isdir(itemPath):
 412 					item += "/"
 413 					fileType = "Directory"
 414 				content.append("""
 415 					<tr>
 416 						<td class="name"><a href="%s">%s</a></td>
 417 						<td class="last-modified">%s</td>
 418 						<td class="size">%s</td>
 419 						<td class="type">%s</td>
 420 					</tr>""" % (urllib.quote(item), item, lastModified, fileSize, fileType))
 421 
 422 		listing = header + "\n".join(content) + footer
 423 
 424 		# write listing
 425 		self.send_response(200)
 426 		self.send_header("Content-Type", "text/html")
 427 		if head:
 428 			self.end_headers()
 429 			return
 430 		self.send_header("Content-Length", str(len(listing)))
 431 		self.end_headers()
 432 		self.wfile.write(listing)
 433 
 434 	def convertSize(self, size):
 435 		for ext in "KMGT":
 436 			size /= 1024.0
 437 			if size < 1024.0:
 438 				break
 439 		if ext == "K" and size < 0.1:
 440 			size = 0.1
 441 		return (size, ext.strip())
 442 
 443 	def getCleanPath(self):
 444 		urlPath = posixpath.normpath(urllib.unquote(self.path)).strip("/")
 445 		path = os.path.join(self.targetDir, urlPath)
 446 		return path
 447 
 448 
 449 class FilePutter(BaseHTTPServer.BaseHTTPRequestHandler):
 450 	""" Simple HTTP Server which allows uploading to a specified directory
 451 	either via multipart/form-data or POST/PUT requests containing the file.
 452 	"""
 453 
 454 	targetDir = None
 455 	maxUploadSize = 0
 456 	blockSize = 1024 * 1024
 457 	uploadPage = """
 458 <!docype html>
 459 <html>
 460 	<form action="/" method="post" enctype="multipart/form-data">
 461 		<label for="file">Filename:</label>
 462 		<input type="file" name="file" id="file" />
 463 		<br />
 464 		<input type="submit" name="submit" value="Upload" />
 465 	</form>
 466 </html>
 467 """
 468 
 469 	def do_GET(self):
 470 		""" Answer every GET request with the upload form """
 471 		self.sendResponse(200, self.uploadPage)
 472 
 473 	def do_POST(self):
 474 		""" Upload a file via POST
 475 
 476 		If the content-type is multipart/form-data it checks for the file
 477 		field and saves the data to disk. For other content-types it just
 478 		calls do_PUT and is handled as such except for the http response code.
 479 
 480 		Files can be uploaded with wget --post-file=path/to/file <url> or
 481 		curl -X POST -d @file <url> .
 482 		"""
 483 		length = self.getContentLength()
 484 		if length < 0:
 485 			return
 486 		ctype = self.headers.getheader('Content-Type')
 487 
 488 		# check for multipart/form-data.
 489 		if not (ctype and ctype.lower().startswith("multipart/form-data")):
 490 			# not a normal multipart request ==> handle as PUT request
 491 			return self.do_PUT(fromPost=True)
 492 
 493 		# create FieldStorage object for multipart parsing
 494 		env = os.environ
 495 		env['REQUEST_METHOD'] = "POST"
 496 		fstorage = cgi.FieldStorage(fp=self.rfile, headers=self.headers, environ=env)
 497 		if not "file" in fstorage:
 498 			self.sendResponse(400, "No file found in request.")
 499 			return
 500 
 501 		destFileName = self.getTargetName(fstorage["file"].filename)
 502 		if destFileName == "":
 503 			self.sendResponse(400, "Filename was empty or invalid")
 504 			return
 505 
 506 		# write file down to disk, send a 200 afterwards
 507 		target = open(destFileName, "w")
 508 		bytesLeft = length
 509 		while bytesLeft > 0:
 510 			bytesToRead = min(self.blockSize, bytesLeft)
 511 			target.write(fstorage["file"].file.read(bytesToRead))
 512 			bytesLeft -= bytesToRead
 513 		target.close()
 514 		self.sendResponse(200, "OK! Thanks for uploading")
 515 		print("Received file '%s' from %s." % (destFileName, self.client_address[0]))
 516 
 517 	def do_PUT(self, fromPost=False):
 518 		""" Upload a file via PUT
 519 
 520 		The request path is used as filename, so uploading a file to the url
 521 		http://host:8080/testfile will cause the file to be named testfile. If
 522 		no filename is given, a random name will be generated.
 523 
 524 		Files can be uploaded with e.g. curl -X POST -d @file <url> .
 525 		"""
 526 		length = self.getContentLength()
 527 		if length < 0:
 528 			return
 529 
 530 		fileName = urllib.unquote(self.path)
 531 		if fileName == "/":
 532 			# if no filename was given we have to generate one
 533 			fileName = str(time.time())
 534 
 535 		cleanFileName = self.getTargetName(fileName)
 536 		if cleanFileName == "":
 537 			self.sendResponse(400, "Filename was invalid")
 538 			return
 539 
 540 		# Sometimes clients want to be told to continue with their transfer
 541 		if self.headers.getheader("Expect") == "100-continue":
 542 			self.send_response(100)
 543 			self.end_headers()
 544 
 545 		target = open(cleanFileName, "w")
 546 		bytesLeft = int(self.headers['Content-Length'])
 547 		while bytesLeft > 0:
 548 			bytesToRead = min(self.blockSize, bytesLeft)
 549 			target.write(self.rfile.read(bytesToRead))
 550 			bytesLeft -= bytesToRead
 551 		target.close()
 552 		self.sendResponse(200 if fromPost else 201, "OK!")
 553 
 554 	def getContentLength(self):
 555 		length = 0
 556 		try:
 557 			length = int(self.headers['Content-Length'])
 558 		except (ValueError, KeyError):
 559 			pass
 560 		if length <= 0:
 561 			self.sendResponse(411, "Content-Length was invalid or not set.")
 562 			return -1
 563 		if self.maxUploadSize > 0 and length > self.maxUploadSize:
 564 			self.sendResponse(413, "Your file was too big! Maximum allowed size is %d byte. <a href=\"/\">back</a>" % self.maxUploadSize)
 565 			return -1
 566 		return length
 567 
 568 	def sendResponse(self, code, msg):
 569 		""" Send a HTTP response with HTTP statuscode code and message msg,
 570 		providing the correct content-length.
 571 		"""
 572 		self.send_response(code)
 573 		self.send_header('Content-Type', 'text/html')
 574 		self.send_header('Content-Length', str(len(msg)))
 575 		self.end_headers()
 576 		self.wfile.write(msg)
 577 
 578 	def getTargetName(self, fname):
 579 		""" Generate a clean and secure filename.
 580 
 581 		This function takes a filename and strips all the slashes out of it.
 582 		If the file already exists in the target directory, a (NUM) will be
 583 		appended, so no file will be overwritten.
 584 		"""
 585 		cleanFileName = fname.replace("/", "")
 586 		if cleanFileName == "":
 587 			return ""
 588 		destFileName = os.path.join(self.targetDir, cleanFileName)
 589 		if not os.path.exists(destFileName):
 590 			return destFileName
 591 		else:
 592 			i = 1
 593 			extraDestFileName = destFileName + "(%s)" % i
 594 			while os.path.exists(extraDestFileName):
 595 				i += 1
 596 				extraDestFileName = destFileName + "(%s)" % i
 597 			return extraDestFileName
 598 		# never reached
 599 
 600 class ThreadedHTTPServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
 601 	pass
 602 
 603 def catchSSLErrors(BaseSSLClass):
 604 	""" Class decorator which catches SSL errors and prints them. """
 605 	class X(BaseSSLClass):
 606 		def handle_one_request(self, *args, **kwargs):
 607 			try:
 608 				BaseSSLClass.handle_one_request(self, *args, **kwargs)
 609 			except SSL.Error as e:
 610 				if str(e) == "":
 611 					print("%s SSL error (empty error message)" % (self.client_address[0],))
 612 				else:
 613 					print("%s SSL error: %s" % (self.client_address[0], e))
 614 	return X
 615 
 616 
 617 class SecureThreadedHTTPServer(ThreadedHTTPServer):
 618 	def __init__(self, pubKey, privKey, server_address, RequestHandlerClass, bind_and_activate=True):
 619 		ThreadedHTTPServer.__init__(self, server_address, RequestHandlerClass, bind_and_activate)
 620 		ctx = SSL.Context(SSL.SSLv23_METHOD)
 621 		if type(pubKey) is crypto.X509 and type(privKey) is crypto.PKey:
 622 			ctx.use_certificate(pubKey)
 623 			ctx.use_privatekey(privKey)
 624 		else:
 625 			ctx.use_certificate_file(pubKey)
 626 			ctx.use_privatekey_file(privKey)
 627 
 628 		self.bsocket = socket.socket(self.address_family, self.socket_type)
 629 		self.socket = SSL.Connection(ctx, self.bsocket)
 630 
 631 		if bind_and_activate:
 632 			self.server_bind()
 633 			self.server_activate()
 634 
 635 	def shutdown_request(self, request):
 636 		request.shutdown()
 637 
 638 
 639 class SecureHandler():
 640 	def setup(self):
 641 		self.connection = self.request
 642 		self.rfile = socket._fileobject(self.request, "rb", self.rbufsize)
 643 		self.wfile = socket._fileobject(self.request, "wb", self.wbufsize)
 644 
 645 class ServeFileException(Exception):
 646 	pass
 647 
 648 
 649 class ServeFile():
 650 	""" Main class to manage everything. """
 651 
 652 	_NUM_MODES = 4
 653 	(MODE_SINGLE, MODE_SINGLETAR, MODE_UPLOAD, MODE_LISTDIR) = range(_NUM_MODES)
 654 
 655 	def __init__(self, target, port=8080, serveMode=0, useSSL=False):
 656 		self.target = target
 657 		self.port = port
 658 		self.serveMode = serveMode
 659 		self.dirCreated = False
 660 		self.useSSL = useSSL
 661 		self.cert = self.key = None
 662 		self.auth = None
 663 		self.maxUploadSize = 0
 664 		self.listenIPv4 = True
 665 		self.listenIPv6 = True
 666 
 667 		if self.serveMode not in range(self._NUM_MODES):
 668 			self.serveMode = None
 669 			raise ValueError("Unknown serve mode, needs to be MODE_SINGLE, MODE_SINGLETAR, MODE_UPLOAD or MODE_DIRLIST.")
 670 
 671 	def setIPv4(self, ipv4):
 672 		""" En- or disable ipv4 """
 673 		self.listenIPv4 = ipv4
 674 
 675 	def setIPv6(self, ipv6):
 676 		""" En- or disable ipv6 """
 677 		self.listenIPv6 = ipv6
 678 
 679 	def getIPs(self):
 680 		""" Get IPs from all interfaces via ip or ifconfig. """
 681 		# ip and ifconfig sometimes are located in /sbin/
 682 		os.environ['PATH'] += ':/sbin:/usr/sbin'
 683 		proc = Popen(r"ip addr|" + \
 684 					  "sed -n -e 's/.*inet6\{0,1\} \([0-9.a-fA-F:]\+\).*/\\1/ p'|" + \
 685 					  "grep -v '^fe80\|^127.0.0.1\|^::1'", \
 686 					  shell=True, stdout=PIPE, stderr=PIPE)
 687 		if proc.wait() != 0:
 688 			# ip failed somehow, falling back to ifconfig
 689 			oldLang = os.environ.get("LC_ALL", None)
 690 			os.environ['LC_ALL'] = "C"
 691 			proc = Popen(r"ifconfig|" + \
 692 						  "sed -n 's/.*inet6\{0,1\}\( addr:\)\{0,1\} \{0,1\}\([0-9a-fA-F.:]*\).*/" + \
 693 						  "\\2/p'|" + \
 694 						  "grep -v '^fe80\|^127.0.0.1\|^::1'", \
 695 						  shell=True, stdout=PIPE, stderr=PIPE)
 696 			if oldLang:
 697 				os.environ['LC_ALL'] = oldLang
 698 			else:
 699 				del(os.environ['LC_ALL'])
 700 			if proc.wait() != 0:
 701 				# we couldn't find any ip address
 702 				proc = None
 703 		if proc:
 704 			ips = proc.stdout.read().strip().split("\n")
 705 
 706 			# filter out ips we are not listening on
 707 			if not self.listenIPv6:
 708 				ips = filter(lambda ip: ":" not in ip, ips)
 709 			if not self.listenIPv4:
 710 				ips = filter(lambda ip: "." not in ip, ips)
 711 
 712 			return ips
 713 		return None
 714 
 715 	def setSSLKeys(self, cert, key):
 716 		""" Set SSL cert/key. Can be either path to file or pyssl X509/PKey object. """
 717 		self.cert = cert
 718 		self.key = key
 719 
 720 	def setMaxUploadSize(self, limit):
 721 		""" Set the maximum upload size in byte """
 722 		self.maxUploadSize = limit
 723 
 724 	def setCompression(self, compression):
 725 		""" Set the compression of TarFileHandler """
 726 		if self.serveMode != self.MODE_SINGLETAR:
 727 			raise ServeFileException("Compression mode can only be set in tar-mode.")
 728 		if compression not in TarFileHandler.compressionMethods:
 729 			raise ServeFileException("Compression mode not available.")
 730 		TarFileHandler.compression = compression
 731 
 732 	def genKeyPair(self):
 733 		print("Generating SSL certificate...", end="")
 734 		sys.stdout.flush()
 735 
 736 		pkey = crypto.PKey()
 737 		pkey.generate_key(crypto.TYPE_RSA, 2048)
 738 
 739 		req = crypto.X509Req()
 740 		subj = req.get_subject()
 741 		subj.CN = "127.0.0.1"
 742 		subj.O = "servefile laboratories"
 743 		subj.OU = "servefile"
 744 
 745 		# generate altnames
 746 		altNames = []
 747 		for ip in self.getIPs() + ["127.0.0.1", "::1"]:
 748 			altNames.append("IP:%s" % ip)
 749 		altNames.append("DNS:localhost")
 750 		ext = crypto.X509Extension("subjectAltName", False, ",".join(altNames))
 751 		req.add_extensions([ext])
 752 
 753 		req.set_pubkey(pkey)
 754 		req.sign(pkey, "sha1")
 755 
 756 		cert = crypto.X509()
 757 		# some browsers complain if they see a cert from the same authority
 758 		# with the same serial ==> we just use the seconds as serial.
 759 		cert.set_serial_number(int(time.time()))
 760 		cert.gmtime_adj_notBefore(0)
 761 		cert.gmtime_adj_notAfter(365*24*60*60)
 762 		cert.set_issuer(req.get_subject())
 763 		cert.set_subject(req.get_subject())
 764 		cert.add_extensions([ext])
 765 		cert.set_pubkey(req.get_pubkey())
 766 		cert.sign(pkey, "sha1")
 767 
 768 		self.cert = cert
 769 		self.key = pkey
 770 
 771 		print("done.")
 772 		print("SHA1 fingerprint:", cert.digest("sha1"))
 773 		print("MD5  fingerprint:", cert.digest("md5"))
 774 
 775 	def _getCert(self):
 776 		return self.cert
 777 
 778 	def _getKey(self):
 779 		return self.key
 780 
 781 	def setAuth(self, user, password, realm=None):
 782 		if not user or not password:
 783 			raise ServeFileException("User and password both need to be at least one character.")
 784 		self.auth = base64.b64encode("%s:%s" % (user, password))
 785 		self.authrealm = realm
 786 
 787 	def _createServer(self, handler, withv6=False):
 788 		ThreadedHTTPServer.address_family = socket.AF_INET
 789 		SecureThreadedHTTPServer.address_family = socket.AF_INET
 790 		listenIp = ''
 791 		server = None
 792 
 793 		if withv6:
 794 			listenIp = '::'
 795 			ThreadedHTTPServer.address_family = socket.AF_INET6
 796 			SecureThreadedHTTPServer.address_family = socket.AF_INET6
 797 
 798 		if self.useSSL:
 799 			if not self._getKey():
 800 				self.genKeyPair()
 801 			try:
 802 				server = SecureThreadedHTTPServer(self._getCert(), self._getKey(),
 803 									(listenIp, self.port), handler, bind_and_activate=False)
 804 			except SSL.Error as e:
 805 				raise ServeFileException("SSL error: Could not read SSL public/private key from file(s) (error was: \"%s\")" % (e[0][0][2],))
 806 		else:
 807 			server = ThreadedHTTPServer((listenIp, self.port), handler,
 808 												bind_and_activate=False)
 809 
 810 		if withv6:
 811 			server.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
 812 
 813 		server.server_bind()
 814 		server.server_activate()
 815 
 816 		return server
 817 
 818 	def serve(self):
 819 		self.handler = self._confAndFindHandler()
 820 		self.server = []
 821 
 822 		try:
 823 			currsocktype = "IPv4"
 824 			if self.listenIPv4:
 825 				self.server.append(self._createServer(self.handler))
 826 			currsocktype = "IPv6"
 827 			if self.listenIPv6:
 828 				self.server.append(self._createServer(self.handler, withv6=True))
 829 		except socket.error as e:
 830 			raise ServeFileException("Could not open %s socket: %s" % (currsocktype, e))
 831 
 832 		if self.serveMode != self.MODE_UPLOAD:
 833 			print("Serving \"%s\" at port %d." % (self.target, self.port))
 834 		else:
 835 			print("Serving \"%s\" for uploads at port %d." % (self.target, self.port))
 836 
 837 		# print urls with local network adresses
 838 		print("\nSome addresses %s will be available at:" % \
 839 				("this file" if (self.serveMode != self.MODE_UPLOAD) else "the uploadform", ))
 840 		ips = self.getIPs()
 841 		if not ips or len(ips) == 0 or ips[0] == '':
 842 			print("Could not find any addresses.")
 843 		else:
 844 			for ip in ips:
 845 				if ":" in ip:
 846 					ip = "[%s]" % ip
 847 				print("\thttp%s://%s:%d/" % (self.useSSL and "s" or "", ip, self.port))
 848 		print()
 849 
 850 		try:
 851 			while True:
 852 				(servers, _, _) = select.select(self.server, [], [])
 853 				for server in servers:
 854 					server.handle_request()
 855 		except KeyboardInterrupt:
 856 			for server in self.server:
 857 				server.socket.close()
 858 
 859 		# cleanup potential upload directory
 860 		if self.dirCreated and len(os.listdir(self.target)) == 0:
 861 			# created upload dir was not used
 862 			os.rmdir(self.target)
 863 
 864 	def _confAndFindHandler(self):
 865 		handler = None
 866 		if self.serveMode == self.MODE_SINGLE:
 867 			try:
 868 				testit = open(self.target, 'r')
 869 				testit.close()
 870 			except IOError:
 871 				raise ServeFileException("Error: Could not open file!")
 872 			FileHandler.filePath = self.target
 873 			FileHandler.fileName = os.path.basename(self.target)
 874 			FileHandler.fileLength = os.stat(self.target).st_size
 875 			handler = FileHandler
 876 		elif self.serveMode == self.MODE_SINGLETAR:
 877 			self.realTarget = os.path.realpath(self.target)
 878 			if not os.path.exists(self.realTarget):
 879 				raise ServeFileException("Error: Could not open file or directory.")
 880 			TarFileHandler.target = self.realTarget
 881 			TarFileHandler.fileName = os.path.basename(self.realTarget.rstrip("/")) + TarFileHandler.getCompressionExt()
 882 
 883 			handler = TarFileHandler
 884 		elif self.serveMode == self.MODE_UPLOAD:
 885 			if os.path.isdir(self.target):
 886 				print("Warning: Uploading to an already existing directory.")
 887 			elif not os.path.exists(self.target):
 888 				self.dirCreated = True
 889 				try:
 890 					os.mkdir(self.target)
 891 				except (IOError, OSError):
 892 					raise ServeFileException("Error: Could not create directory '%s' for uploads." % (self.target,))
 893 			else:
 894 				raise ServeFileException("Error: Upload directory already exists and is a file.")
 895 			FilePutter.targetDir = self.target
 896 			FilePutter.maxUploadSize = self.maxUploadSize
 897 			handler = FilePutter
 898 		elif self.serveMode == self.MODE_LISTDIR:
 899 			if not os.path.exists(self.target):
 900 				raise ServeFileException("Error: Could not open file or directory.")
 901 			if not os.path.isdir(self.target):
 902 				raise ServeFileException("Error: '%s' is not a directory." % (self.target,))
 903 			handler = DirListingHandler
 904 			handler.targetDir = self.target
 905 
 906 		if self.auth:
 907 			# do authentication
 908 			AuthenticationHandler.authString = self.auth
 909 			if self.authrealm:
 910 				AuthenticationHandler.realm = self.authrealm
 911 			class AuthenticatedHandler(AuthenticationHandler, handler):
 912 				pass
 913 			handler = AuthenticatedHandler
 914 
 915 		if self.useSSL:
 916 			# secure handler
 917 			@catchSSLErrors
 918 			class AlreadySecuredHandler(SecureHandler, handler):
 919 				pass
 920 			handler = AlreadySecuredHandler
 921 		return handler
 922 
 923 
 924 class AuthenticationHandler():
 925 	# base64 encoded user:password string for authentication
 926 	authString = None
 927 	realm = "Restricted area"
 928 
 929 	def handle_one_request(self):
 930 		""" Overloaded function to handle one request.
 931 
 932 		Before calling the responsible do_METHOD function, check credentials
 933 		"""
 934 		self.raw_requestline = self.rfile.readline()
 935 		if not self.raw_requestline:
 936 			self.close_connection = 1
 937 			return
 938 		if not self.parse_request(): # An error code has been sent, just exit
 939 			return
 940 
 941 		authorized = False
 942 		if "Authorization" in self.headers:
 943 			if self.headers["Authorization"] == ("Basic " + self.authString):
 944 				authorized = True
 945 		if authorized:
 946 			mname = 'do_' + self.command
 947 			if not hasattr(self, mname):
 948 				self.send_error(501, "Unsupported method (%r)" % self.command)
 949 				return
 950 			method = getattr(self, mname)
 951 			method()
 952 		else:
 953 			self.send_response(401)
 954 			self.send_header("WWW-Authenticate", "Basic realm=\"%s\"" % self.realm)
 955 
 956 
 957 def main():
 958 	parser = argparse.ArgumentParser(description='Serve a single file via HTTP.')
 959 	parser.add_argument('--version', action='version', version='%(prog)s ' + __version__)
 960 	parser.add_argument('target', metavar='file/directory', type=str)
 961 	parser.add_argument('-p', '--port', type=int, default=8080, \
 962 	                    help='Port to listen on')
 963 	parser.add_argument('-u', '--upload', action="store_true", default=False, \
 964 	                    help="Enable uploads to a given directory")
 965 	parser.add_argument('-s', '--max-upload-size', type=str, \
 966 	                    help="Limit upload size in kB. Size modifiers are allowed, e.g. 2G, 12MB, 1B")
 967 	parser.add_argument('-l', '--list-dir', action="store_true", default=False, \
 968 	                    help="Show directory indexes and allow access to all subdirectories")
 969 	parser.add_argument('--ssl', action="store_true", default=False, \
 970 	                    help="Enable SSL. If no key/cert is specified one will be generated")
 971 	parser.add_argument('--key', type=str, \
 972 	                    help="Keyfile to use for SSL. If no cert is given with --cert the keyfile will also be searched for a cert")
 973 	parser.add_argument('--cert', type=str, \
 974 	                    help="Certfile to use for SSL")
 975 	parser.add_argument('-a', '--auth', type=str, metavar='user:password', \
 976 	                    help="Set user and password for HTTP basic authentication")
 977 	parser.add_argument('--realm', type=str, default=None,\
 978 	                    help="Set a realm for HTTP basic authentication")
 979 	parser.add_argument('-t', '--tar', action="store_true", default=False, \
 980 	                    help="Enable on the fly tar creation for given file or directory. Note: Download continuation will not be available")
 981 	parser.add_argument('-c', '--compression', type=str, metavar='method', \
 982 	                    default="none", \
 983 	                    help="Set compression method, only in combination with --tar. Can be one of %s" % ", ".join(TarFileHandler.compressionMethods))
 984 	parser.add_argument('-4', '--ipv4-only', action="store_true", default=False, \
 985 	                    help="Listen on IPv4 only")
 986 	parser.add_argument('-6', '--ipv6-only', action="store_true", default=False, \
 987 	                    help="Listen on IPv6 only")
 988 
 989 	args = parser.parse_args()
 990 	maxUploadSize = 0
 991 
 992 	# check for invalid option combinations/preparse stuff
 993 	if args.max_upload_size and not args.upload:
 994 		print("Error: Maximum upload size can only be specified when in upload mode.")
 995 		sys.exit(1)
 996 
 997 	if args.upload and args.list_dir:
 998 		print("Error: Upload and dirlisting can't be enabled together.")
 999 		sys.exit(1)
1000 
1001 	if args.max_upload_size:
1002 		sizeRe = re.match("^(\d+(?:[,.]\d+)?)(?:([bkmgtpe])(?:(?<!b)b?)?)?$", args.max_upload_size.lower())
1003 		if not sizeRe:
1004 			print("Error: Your max upload size param is broken. Try something like 3M or 2.5Gb.")
1005 			sys.exit(1)
1006 		uploadSize, modifier = sizeRe.groups()
1007 		uploadSize = float(uploadSize.replace(",", "."))
1008 		sizes = ["b", "k", "m", "g", "t", "p", "e"]
1009 		maxUploadSize = int(uploadSize * pow(1024, sizes.index(modifier or "k")))
1010 		if maxUploadSize < 0:
1011 			print("Error: Your max upload size can't be negative")
1012 			sys.exit(1)
1013 
1014 	if args.ssl and not HAVE_SSL:
1015 		print("Error: SSL is not available, please install pyssl (python-openssl).")
1016 		sys.exit(1)
1017 
1018 	if args.cert and not args.key:
1019 		print("Error: Please specify a key along with your cert.")
1020 		sys.exit(1)
1021 
1022 	if not args.ssl and (args.cert or args.key):
1023 		print("Error: You need to enable ssl with --ssl when specifying certs/keys.")
1024 		sys.exit(1)
1025 
1026 	if args.auth:
1027 		dpos = args.auth.find(":")
1028 		if dpos <= 0 or dpos == (len(args.auth)-1):
1029 			print("Error: User and password for HTTP basic authentication need to be both at least one character and have to be separated by a \":\".")
1030 			sys.exit(1)
1031 
1032 	if args.realm and not args.auth:
1033 		print("You can only specify a realm when HTTP basic authentication is enabled.")
1034 		sys.exit(1)
1035 
1036 	if args.compression != "none" and not args.tar:
1037 		print("Error: Please use --tar if you want to tar everything.")
1038 		sys.exit(1)
1039 
1040 	if args.tar and args.upload:
1041 		print("Error: --tar mode will not work with uploads.")
1042 		sys.exit(1)
1043 
1044 	if args.tar and args.list_dir:
1045 		print("Error: --tar mode will not work with directory listings.")
1046 		sys.exit(1)
1047 
1048 	compression = None
1049 	if args.compression:
1050 		if args.compression in TarFileHandler.compressionMethods:
1051 			compression = args.compression
1052 		else:
1053 			print("Error: Compression mode '%s' is unknown." % args.compression)
1054 			sys.exit(1)
1055 
1056 	if args.ipv4_only and args.ipv6_only:
1057 		print("You can't listen both on IPv4 and IPv6 \"only\".")
1058 		sys.exit(1)
1059 
1060 	if args.ipv6_only and not socket.has_ipv6:
1061 		print("Your system does not support IPv6.")
1062 		sys.exit(1)
1063 
1064 	mode = None
1065 	if args.upload:
1066 		mode = ServeFile.MODE_UPLOAD
1067 	elif args.list_dir:
1068 		mode = ServeFile.MODE_LISTDIR
1069 	elif args.tar:
1070 		mode = ServeFile.MODE_SINGLETAR
1071 	else:
1072 		mode = ServeFile.MODE_SINGLE
1073 
1074 	server = None
1075 	try:
1076 		server = ServeFile(args.target, args.port, mode, args.ssl)
1077 		if maxUploadSize > 0:
1078 			server.setMaxUploadSize(maxUploadSize)
1079 		if args.ssl and args.key:
1080 			cert = args.cert or args.key
1081 			server.setSSLKeys(cert, args.key)
1082 		if args.auth:
1083 			user, password = args.auth.split(":", 1)
1084 			server.setAuth(user, password, args.realm)
1085 		if compression and compression != "none":
1086 			server.setCompression(compression)
1087 		if args.ipv4_only or not socket.has_ipv6:
1088 			server.setIPv6(False)
1089 		if args.ipv6_only:
1090 			server.setIPv4(False)
1091 		server.serve()
1092 	except ServeFileException as e:
1093 		print(e)
1094 		sys.exit(1)
1095 	print("Good bye.")
1096 
1097 
1098 if __name__ == '__main__':
1099 	main()
π