2012-03-12 15:41:55 +01:00
|
|
|
#!/usr/bin/python
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
2012-03-18 04:16:10 +01:00
|
|
|
# Licensed under GNU General Public License v3 or later
|
|
|
|
# Written by Sebastian Lohff (seba@seba-geek.de)
|
|
|
|
# http://seba-geek.de/stuff/servefile/
|
2012-03-18 04:53:28 +01:00
|
|
|
|
2012-06-25 19:23:50 +02:00
|
|
|
from __future__ import print_function
|
|
|
|
|
2012-06-27 01:44:49 +02:00
|
|
|
__version__ = '0.4.2'
|
2012-03-12 15:41:55 +01:00
|
|
|
|
2012-03-18 02:04:58 +01:00
|
|
|
import argparse
|
2012-04-16 16:36:40 +02:00
|
|
|
import base64
|
2012-04-07 02:57:06 +02:00
|
|
|
import cgi
|
2012-04-09 15:32:57 +02:00
|
|
|
import datetime
|
2012-06-20 22:55:43 +02:00
|
|
|
import mimetypes
|
2012-03-12 15:41:55 +01:00
|
|
|
import urllib
|
2012-03-18 01:42:03 +01:00
|
|
|
import os
|
2012-06-20 22:55:43 +02:00
|
|
|
import posixpath
|
2012-04-16 18:40:28 +02:00
|
|
|
import re
|
2012-06-25 02:13:36 +02:00
|
|
|
import select
|
2012-03-12 15:41:55 +01:00
|
|
|
import socket
|
2012-03-18 05:08:49 +01:00
|
|
|
from subprocess import Popen, PIPE
|
2012-03-18 01:42:03 +01:00
|
|
|
import sys
|
2012-04-07 18:50:24 +02:00
|
|
|
import time
|
2012-03-12 15:41:55 +01:00
|
|
|
|
2012-06-25 19:23:50 +02:00
|
|
|
# fix imports for python2/python3
|
|
|
|
try:
|
|
|
|
import BaseHTTPServer
|
|
|
|
import SocketServer
|
|
|
|
except ImportError:
|
|
|
|
# both have different names in python3
|
|
|
|
import http.server as BaseHTTPServer
|
|
|
|
import socketserver as SocketServer
|
|
|
|
|
2012-04-14 22:31:09 +02:00
|
|
|
# only activate SSL if available
|
|
|
|
HAVE_SSL = False
|
|
|
|
try:
|
|
|
|
from OpenSSL import SSL, crypto
|
|
|
|
HAVE_SSL = True
|
|
|
|
except ImportError:
|
|
|
|
pass
|
|
|
|
|
2012-04-09 15:32:57 +02:00
|
|
|
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")
|
2012-03-12 15:41:55 +01:00
|
|
|
|
2012-04-26 15:45:51 +02:00
|
|
|
class FileBaseHandler(BaseHTTPServer.BaseHTTPRequestHandler):
|
2012-06-19 18:24:22 +02:00
|
|
|
fileName = None
|
2012-04-27 03:52:07 +02:00
|
|
|
blockSize = 1024 * 1024
|
2012-06-05 16:33:56 +02:00
|
|
|
server_version = "servefile/" + __version__
|
2012-04-27 03:52:07 +02:00
|
|
|
|
2012-06-16 21:52:11 +02:00
|
|
|
def checkAndDoRedirect(self, fileName=None):
|
2012-04-09 15:32:57 +02:00
|
|
|
""" If request didn't request self.fileName redirect to self.fileName.
|
|
|
|
|
|
|
|
Returns True if a redirect was issued. """
|
2012-06-16 21:52:11 +02:00
|
|
|
if not fileName:
|
|
|
|
fileName = self.fileName
|
|
|
|
if urllib.unquote(self.path) != "/" + fileName:
|
2012-03-12 15:41:55 +01:00
|
|
|
self.send_response(302)
|
2012-06-16 21:52:11 +02:00
|
|
|
self.send_header('Location', '/' + fileName)
|
2012-03-12 15:41:55 +01:00
|
|
|
self.end_headers()
|
2012-04-09 15:32:57 +02:00
|
|
|
return True
|
|
|
|
return False
|
|
|
|
|
2012-06-06 15:54:47 +02:00
|
|
|
def sendContentHeaders(self, fileName, fileLength, lastModified=None):
|
|
|
|
""" Send default Content headers for given fileName and fileLength.
|
|
|
|
|
|
|
|
If no lastModified is given the current date is taken. If
|
|
|
|
fileLength is lesser than 0 no Content-Length will be sent."""
|
|
|
|
if not lastModified:
|
|
|
|
lastModified = getDateStrNow()
|
|
|
|
|
|
|
|
if fileLength >= 0:
|
|
|
|
self.send_header('Content-Length', str(fileLength))
|
|
|
|
self.send_header('Last-Modified', lastModified)
|
|
|
|
self.send_header('Content-Type', 'application/octet-stream')
|
|
|
|
self.send_header('Content-Disposition', 'attachment; filename="%s"' % fileName)
|
|
|
|
self.send_header('Content-Transfer-Encoding', 'binary')
|
|
|
|
|
2012-06-06 17:15:16 +02:00
|
|
|
def isRangeRequest(self):
|
2012-06-19 18:24:22 +02:00
|
|
|
""" Return True if partial content is requestet """
|
2012-06-06 17:15:16 +02:00
|
|
|
return "Range" in self.headers
|
|
|
|
|
|
|
|
def handleRangeRequest(self, fileLength):
|
|
|
|
""" Find out and handle continuing downloads.
|
|
|
|
|
|
|
|
Returns a tuple of a boolean, if this is a valid range request,
|
|
|
|
and a range. When the requested range is out of range, range is
|
|
|
|
set to None.
|
|
|
|
"""
|
|
|
|
fromto = None
|
|
|
|
if self.isRangeRequest():
|
|
|
|
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] = fileLength - 1
|
|
|
|
try:
|
|
|
|
fromto[0] = int(fromto[0])
|
|
|
|
fromto[1] = int(fromto[1])
|
|
|
|
except:
|
|
|
|
return (False, None)
|
|
|
|
|
|
|
|
if fromto[0] >= fileLength or fromto[0] < 0 or fromto[1] >= fileLength or fromto[1]-fromto[0] < 0:
|
|
|
|
# oops, already done! (requested range out of range)
|
|
|
|
self.send_response(416)
|
2012-06-26 02:13:24 +02:00
|
|
|
self.send_header('Content-Range', 'bytes */%d' % fileLength)
|
2012-06-06 17:15:16 +02:00
|
|
|
self.end_headers()
|
|
|
|
return (True, None)
|
|
|
|
return (True, fromto)
|
|
|
|
# broken request or no range header
|
|
|
|
return (False, None)
|
|
|
|
|
2012-06-19 18:26:05 +02:00
|
|
|
def sendFile(self, filePath, fileLength=None, lastModified=None):
|
|
|
|
""" Send file with continuation support.
|
2012-04-26 15:45:51 +02:00
|
|
|
|
2012-06-19 18:26:05 +02:00
|
|
|
filePath: path to file to be sent
|
|
|
|
fileLength: length of file (if None is given this will be found out)
|
|
|
|
lastModified: time the file was last modified, None for "now"
|
|
|
|
"""
|
|
|
|
if not fileLength:
|
2012-06-26 02:13:24 +02:00
|
|
|
fileLength = os.stat(filePath).st_size
|
2012-03-12 15:41:55 +01:00
|
|
|
|
2012-06-19 18:26:05 +02:00
|
|
|
(responseCode, myfile) = self.getFileHandle(filePath)
|
|
|
|
if not myfile:
|
|
|
|
self.send_response(responseCode)
|
|
|
|
self.end_headers()
|
2012-04-13 20:17:48 +02:00
|
|
|
return
|
2012-04-28 17:52:57 +02:00
|
|
|
|
2012-06-19 18:26:05 +02:00
|
|
|
(continueDownload, fromto) = self.handleRangeRequest(fileLength)
|
2012-06-06 17:15:16 +02:00
|
|
|
if continueDownload:
|
|
|
|
if not fromto:
|
|
|
|
# we are done
|
2012-06-19 18:26:05 +02:00
|
|
|
return True
|
2012-06-06 17:15:16 +02:00
|
|
|
|
|
|
|
# now we can wind the file *brrrrrr*
|
|
|
|
myfile.seek(fromto[0])
|
2012-04-28 17:52:57 +02:00
|
|
|
|
2012-03-12 15:41:55 +01:00
|
|
|
if fromto != None:
|
|
|
|
self.send_response(216)
|
2012-06-26 02:13:24 +02:00
|
|
|
self.send_header('Content-Range', 'bytes %d-%d/%d' % (fromto[0], fromto[1], fileLength))
|
2012-06-06 15:54:47 +02:00
|
|
|
fileLength = fromto[1] - fromto[0] + 1
|
2012-03-12 15:41:55 +01:00
|
|
|
else:
|
|
|
|
self.send_response(200)
|
2012-06-19 18:26:05 +02:00
|
|
|
|
|
|
|
fileName = self.fileName
|
|
|
|
if not fileName:
|
|
|
|
fileName = os.path.basename(filePath)
|
|
|
|
self.sendContentHeaders(fileName, fileLength, lastModified)
|
2012-03-12 15:41:55 +01:00
|
|
|
self.end_headers()
|
|
|
|
block = self.getChunk(myfile, fromto)
|
|
|
|
while block:
|
|
|
|
try:
|
|
|
|
self.wfile.write(block)
|
2012-06-25 19:23:50 +02:00
|
|
|
except socket.error as e:
|
|
|
|
print("%s ABORTED transmission (Reason %s: %s)" % (self.client_address[0], e[0], e[1]))
|
2012-06-19 18:26:05 +02:00
|
|
|
return False
|
2012-03-12 15:41:55 +01:00
|
|
|
block = self.getChunk(myfile, fromto)
|
|
|
|
myfile.close()
|
2012-06-25 19:23:50 +02:00
|
|
|
print("%s finished downloading %s" % (self.client_address[0], filePath))
|
2012-06-19 18:26:05 +02:00
|
|
|
return True
|
2012-04-28 17:52:57 +02:00
|
|
|
|
2012-03-12 15:41:55 +01:00
|
|
|
def getChunk(self, myfile, fromto):
|
2012-03-18 00:21:55 +01:00
|
|
|
if fromto and myfile.tell()+self.blockSize >= fromto[1]:
|
2012-03-12 15:41:55 +01:00
|
|
|
readsize = fromto[1]-myfile.tell()+1
|
|
|
|
else:
|
|
|
|
readsize = self.blockSize
|
|
|
|
return myfile.read(readsize)
|
|
|
|
|
2012-06-19 18:26:05 +02:00
|
|
|
def getFileHandle(self, path):
|
|
|
|
""" Get handle to a file.
|
|
|
|
|
|
|
|
Return a tuple of HTTP response code and file handle.
|
|
|
|
If the handle couldn't be acquired it is set to None
|
|
|
|
and an appropriate HTTP error code is returned.
|
|
|
|
"""
|
|
|
|
myfile = None
|
|
|
|
responseCode = 200
|
|
|
|
try:
|
|
|
|
myfile = open(path, 'rb')
|
2012-06-25 19:23:50 +02:00
|
|
|
except IOError as e:
|
2012-06-19 18:26:05 +02:00
|
|
|
responseCode = self.getResponseForErrno(e.errno)
|
|
|
|
return (responseCode, myfile)
|
|
|
|
|
|
|
|
def getFileLength(self, path):
|
|
|
|
""" Get length of a file.
|
|
|
|
|
|
|
|
Return a tuple of HTTP response code and file length.
|
|
|
|
If filelength couldn't be determined, it is set to -1
|
|
|
|
and an appropriate HTTP error code is returned.
|
|
|
|
"""
|
|
|
|
fileSize = -1
|
|
|
|
responseCode = 200
|
|
|
|
try:
|
2012-06-26 02:13:24 +02:00
|
|
|
fileSize = os.stat(path).st_size
|
2012-06-25 19:23:50 +02:00
|
|
|
except IOError as e:
|
2012-06-19 18:26:05 +02:00
|
|
|
responseCode = self.getResponseForErrno(e.errno)
|
|
|
|
return (responseCode, fileSize)
|
|
|
|
|
|
|
|
def getResponseForErrno(self, errno):
|
|
|
|
""" Return HTTP response code for an IOError errno """
|
2012-06-26 02:13:24 +02:00
|
|
|
if errno == errno.ENOENT:
|
2012-06-19 18:26:05 +02:00
|
|
|
return 404
|
2012-06-26 02:13:24 +02:00
|
|
|
elif errno == errno.EACCESS:
|
2012-06-19 18:26:05 +02:00
|
|
|
return 403
|
|
|
|
else:
|
|
|
|
return 500
|
|
|
|
|
|
|
|
|
|
|
|
class FileHandler(FileBaseHandler):
|
|
|
|
filePath = "/dev/null"
|
|
|
|
fileLength = 0
|
|
|
|
startTime = getDateStrNow()
|
|
|
|
|
|
|
|
def do_HEAD(self):
|
|
|
|
if self.checkAndDoRedirect():
|
|
|
|
return
|
|
|
|
self.send_response(200)
|
|
|
|
self.sendContentHeaders(self.fileName, self.fileLength, self.startTime)
|
|
|
|
self.end_headers()
|
|
|
|
|
|
|
|
def do_GET(self):
|
|
|
|
if self.checkAndDoRedirect():
|
|
|
|
return
|
|
|
|
self.sendFile(self.filePath, self.fileLength, self.startTime)
|
|
|
|
|
|
|
|
|
2012-04-27 03:52:07 +02:00
|
|
|
class TarFileHandler(FileBaseHandler):
|
|
|
|
target = None
|
|
|
|
compression = "none"
|
|
|
|
compressionMethods = ("none", "gzip", "bzip2")
|
|
|
|
|
|
|
|
def do_HEAD(self):
|
|
|
|
if self.checkAndDoRedirect():
|
|
|
|
return
|
|
|
|
self.send_response(200)
|
2012-06-06 15:54:47 +02:00
|
|
|
self.sendContentHeaders(self.fileName, -1)
|
2012-04-27 03:52:07 +02:00
|
|
|
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
|
2012-06-25 19:23:50 +02:00
|
|
|
print("Error while compressing '%s'. Aborting request." % self.target)
|
2012-04-27 03:52:07 +02:00
|
|
|
self.send_response(500)
|
|
|
|
self.end_headers()
|
|
|
|
return
|
|
|
|
|
|
|
|
self.send_response(200)
|
2012-06-06 15:54:47 +02:00
|
|
|
self.sendContentHeaders(self.fileName, -1)
|
2012-04-27 03:52:07 +02:00
|
|
|
self.end_headers()
|
|
|
|
|
|
|
|
block = True
|
|
|
|
while block and block != '':
|
|
|
|
block = tarCmd.stdout.read(self.blockSize)
|
|
|
|
if block and block != '':
|
|
|
|
self.wfile.write(block)
|
2012-06-25 19:23:50 +02:00
|
|
|
print("%s finished downloading" % (self.client_address[0]))
|
2012-04-27 03:52:07 +02:00
|
|
|
|
|
|
|
def getCompressionCmd(self):
|
|
|
|
if self.compression == "none":
|
|
|
|
cmd = ["tar", "-c"]
|
|
|
|
elif self.compression == "gzip":
|
|
|
|
cmd = ["tar", "-cz"]
|
|
|
|
elif self.compression == "bzip2":
|
|
|
|
cmd = ["tar", "-cj"]
|
|
|
|
else:
|
2012-05-04 13:17:40 +02:00
|
|
|
raise ValueError("Unknown compression mode '%s'." % self.compression)
|
2012-04-27 17:39:12 +02:00
|
|
|
|
2012-04-27 03:52:07 +02:00
|
|
|
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
|
|
|
|
|
2012-04-27 17:39:12 +02:00
|
|
|
@staticmethod
|
|
|
|
def getCompressionExt():
|
|
|
|
if TarFileHandler.compression == "none":
|
|
|
|
return ".tar"
|
|
|
|
elif TarFileHandler.compression == "gzip":
|
|
|
|
return ".tar.gz"
|
|
|
|
elif TarFileHandler.compression == "bzip2":
|
|
|
|
return ".tar.bz2"
|
2012-06-16 21:52:11 +02:00
|
|
|
raise ValueError("Unknown compression mode '%s'." % TarFileHandler.compression)
|
2012-04-27 17:39:12 +02:00
|
|
|
|
2012-06-20 22:55:43 +02:00
|
|
|
|
|
|
|
class DirListingHandler(FileBaseHandler):
|
|
|
|
""" DOCUMENTATION MISSING """
|
|
|
|
|
|
|
|
targetDir = None
|
|
|
|
|
|
|
|
def do_HEAD(self):
|
|
|
|
self.getFileOrDirectory(head=True)
|
|
|
|
|
|
|
|
def do_GET(self):
|
|
|
|
self.getFileOrDirectory(head=False)
|
|
|
|
|
|
|
|
def getFileOrDirectory(self, head=False):
|
2012-06-22 13:52:05 +02:00
|
|
|
""" Send file or directory index, depending on requested path """
|
2012-06-20 22:55:43 +02:00
|
|
|
path = self.getCleanPath()
|
|
|
|
|
|
|
|
if os.path.isdir(path):
|
|
|
|
if not self.path.endswith('/'):
|
|
|
|
self.send_response(301)
|
|
|
|
self.send_header("Location", self.path + '/')
|
|
|
|
self.end_headers()
|
|
|
|
else:
|
|
|
|
self.sendDirectoryListing(path, head)
|
|
|
|
elif os.path.isfile(path):
|
|
|
|
if head:
|
|
|
|
(response, length) = self.getFileLength(path)
|
|
|
|
if length < 0:
|
|
|
|
self.send_response(response)
|
|
|
|
self.end_headers()
|
|
|
|
else:
|
|
|
|
self.send_response(200)
|
|
|
|
self.sendContentHeaders(self, path, length)
|
|
|
|
self.end_headers()
|
|
|
|
else:
|
|
|
|
self.sendFile(path, head)
|
|
|
|
else:
|
|
|
|
self.send_response(404)
|
2012-06-26 02:16:02 +02:00
|
|
|
errorMsg = """<!DOCTYPE html><html>
|
|
|
|
<head><title>404 Not Found</title></head>
|
|
|
|
<body>
|
|
|
|
<h1>Not Found</h1>
|
|
|
|
<p>The requestet URL %s was not found on this server</p>
|
|
|
|
<p><a href="/">Back to /</a>
|
|
|
|
</body>
|
|
|
|
</html>""" % self.escapeHTML(urllib.unquote(self.path))
|
|
|
|
self.send_header("Content-Length", str(len(errorMsg)))
|
2012-06-20 22:55:43 +02:00
|
|
|
self.end_headers()
|
2012-06-26 02:16:02 +02:00
|
|
|
if not head:
|
|
|
|
self.wfile.write(errorMsg)
|
|
|
|
|
|
|
|
def escapeHTML(self, htmlstr):
|
|
|
|
entities = [("<", "<"), (">", ">")]
|
|
|
|
for src, dst in entities:
|
|
|
|
htmlstr = htmlstr.replace(src, dst)
|
|
|
|
return htmlstr
|
2012-06-20 22:55:43 +02:00
|
|
|
|
2012-06-25 21:29:10 +02:00
|
|
|
def _appendToListing(self, content, item, itemPath, stat, is_dir):
|
|
|
|
# Strings to display on directory listing
|
|
|
|
lastModifiedDate = datetime.datetime.fromtimestamp(stat.st_mtime)
|
|
|
|
lastModified = lastModifiedDate.strftime("%Y-%m-%d %H:%M")
|
|
|
|
fileSize = "%.1f%s" % self.convertSize(stat.st_size)
|
|
|
|
(fileType, _) = mimetypes.guess_type(itemPath)
|
|
|
|
if not fileType:
|
|
|
|
fileType = "-"
|
|
|
|
|
|
|
|
if is_dir:
|
|
|
|
item += "/"
|
|
|
|
fileType = "Directory"
|
|
|
|
content.append("""
|
|
|
|
<tr>
|
|
|
|
<td class="name"><a href="%s">%s</a></td>
|
|
|
|
<td class="last-modified">%s</td>
|
|
|
|
<td class="size">%s</td>
|
|
|
|
<td class="type">%s</td>
|
|
|
|
</tr>
|
|
|
|
""" % (urllib.quote(item), item, lastModified, fileSize, fileType))
|
|
|
|
|
2012-06-20 22:55:43 +02:00
|
|
|
def sendDirectoryListing(self, path, head):
|
2012-06-22 13:52:05 +02:00
|
|
|
""" Generate a directorylisting for path and send it """
|
2012-06-24 22:53:15 +02:00
|
|
|
header = """<!DOCTYPE html>
|
2012-06-20 22:55:43 +02:00
|
|
|
<html>
|
|
|
|
<head>
|
2012-06-24 22:53:15 +02:00
|
|
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
2012-06-20 22:55:43 +02:00
|
|
|
<title>Index of %(path)s</title>
|
2012-06-22 13:52:05 +02:00
|
|
|
<style type="text/css">
|
2012-06-24 22:53:15 +02:00
|
|
|
a { text-decoration: none; color: #0000BB;}
|
|
|
|
a:visited { color: #000066;}
|
|
|
|
a:hover, a:focus, a:active { text-decoration: underline; color: #cc0000; text-indent: 5px; }
|
|
|
|
body { background-color: #eaeaea; padding: 20px 0; margin: 0; font: 400 13px/1.2em Arial, sans-serif; }
|
|
|
|
h1 { margin: 0 10px 12px 10px; font-family: Arial, sans-serif; }
|
|
|
|
div.content { background-color: white; border-color: #ccc; border-width: 1px 0; border-style: solid; padding: 10px 10px 15px 10px; }
|
|
|
|
td { padding-right: 15px; text-align: left; font-family: monospace; }
|
|
|
|
th { font-weight: bold; font-size: 115%%; padding: 0 15px 5px 0; text-align: left; }
|
|
|
|
.size { text-align: right; }
|
|
|
|
.footer { font: 12px monospace; color: #333; margin: 5px 10px 0; }
|
|
|
|
.footer, h1 { text-shadow: 0 1px 0 white; }
|
2012-06-22 13:52:05 +02:00
|
|
|
</style>
|
2012-06-20 22:55:43 +02:00
|
|
|
</head>
|
|
|
|
<body>
|
2012-06-24 22:53:15 +02:00
|
|
|
<h1>Index of %(path)s</h1>
|
|
|
|
<div class="content">
|
|
|
|
<table summary="Directory Listing">
|
|
|
|
<thead>
|
|
|
|
<tr>
|
|
|
|
<th class="name">Name</th>
|
|
|
|
<th class="lastModified">Last Modified</th>
|
|
|
|
<th class="size">Size</th>
|
|
|
|
<th class="type">Type</th>
|
|
|
|
</tr>
|
|
|
|
</thead>
|
|
|
|
<tbody>
|
2012-06-20 22:55:43 +02:00
|
|
|
""" % {'path': posixpath.normpath(urllib.unquote(self.path))}
|
2012-06-24 22:53:15 +02:00
|
|
|
footer = """</tbody></table></div>
|
2012-06-26 02:16:20 +02:00
|
|
|
<div class="footer"><a href="http://seba-geek.de/stuff/servefile/">servefile %(version)s</a></div>
|
2012-06-20 22:55:43 +02:00
|
|
|
</body>
|
|
|
|
</html>""" % {'version': __version__}
|
|
|
|
content = []
|
2012-06-25 21:37:19 +02:00
|
|
|
|
|
|
|
dir_items = list()
|
|
|
|
file_items = list()
|
|
|
|
|
2012-06-25 21:39:37 +02:00
|
|
|
for item in [".."] + sorted(os.listdir(path), key=lambda x:x.lower()):
|
2012-06-20 22:55:43 +02:00
|
|
|
# create path to item
|
|
|
|
itemPath = os.path.join(path, item)
|
|
|
|
|
2012-10-26 22:54:14 +02:00
|
|
|
# Hide "../" in listing of the (virtual) root directory
|
|
|
|
if item == '..' and path == DirListingHandler.targetDir.rstrip('/') + '/':
|
|
|
|
continue
|
|
|
|
|
2012-06-20 22:55:43 +02:00
|
|
|
# try to stat file for size, last modified... continue on error
|
|
|
|
stat = None
|
|
|
|
try:
|
|
|
|
stat = os.stat(itemPath)
|
|
|
|
except IOError:
|
|
|
|
continue
|
|
|
|
|
|
|
|
if os.path.isdir(itemPath):
|
2012-06-25 21:37:19 +02:00
|
|
|
target_items = dir_items
|
|
|
|
else:
|
|
|
|
target_items = file_items
|
|
|
|
target_items.append((item, itemPath, stat))
|
|
|
|
|
|
|
|
# Directories first, then files
|
|
|
|
for (tuple_list, is_dir) in (
|
|
|
|
(dir_items, True),
|
|
|
|
(file_items, False),
|
|
|
|
):
|
|
|
|
for (item, itemPath, stat) in tuple_list:
|
|
|
|
self._appendToListing(content, item, itemPath, stat, is_dir=is_dir)
|
2012-06-20 22:55:43 +02:00
|
|
|
|
|
|
|
listing = header + "\n".join(content) + footer
|
|
|
|
|
|
|
|
# write listing
|
|
|
|
self.send_response(200)
|
|
|
|
self.send_header("Content-Type", "text/html")
|
|
|
|
if head:
|
|
|
|
self.end_headers()
|
|
|
|
return
|
|
|
|
self.send_header("Content-Length", str(len(listing)))
|
|
|
|
self.end_headers()
|
|
|
|
self.wfile.write(listing)
|
|
|
|
|
|
|
|
def convertSize(self, size):
|
2012-06-26 02:18:18 +02:00
|
|
|
for ext in "KMGT":
|
|
|
|
size /= 1024.0
|
2012-06-20 22:55:43 +02:00
|
|
|
if size < 1024.0:
|
|
|
|
break
|
2012-06-26 02:18:18 +02:00
|
|
|
if ext == "K" and size < 0.1:
|
|
|
|
size = 0.1
|
|
|
|
return (size, ext.strip())
|
2012-06-20 22:55:43 +02:00
|
|
|
|
|
|
|
def getCleanPath(self):
|
|
|
|
urlPath = posixpath.normpath(urllib.unquote(self.path)).strip("/")
|
|
|
|
path = os.path.join(self.targetDir, urlPath)
|
|
|
|
return path
|
|
|
|
|
|
|
|
|
2012-04-05 15:56:28 +02:00
|
|
|
class FilePutter(BaseHTTPServer.BaseHTTPRequestHandler):
|
2012-04-12 13:17:55 +02:00
|
|
|
""" Simple HTTP Server which allows uploading to a specified directory
|
|
|
|
either via multipart/form-data or POST/PUT requests containing the file.
|
|
|
|
"""
|
|
|
|
|
2012-04-16 18:40:28 +02:00
|
|
|
targetDir = None
|
|
|
|
maxUploadSize = 0
|
2012-06-26 02:13:24 +02:00
|
|
|
blockSize = 1024 * 1024
|
2012-04-07 02:57:06 +02:00
|
|
|
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>
|
|
|
|
"""
|
|
|
|
|
2012-04-05 15:56:28 +02:00
|
|
|
def do_GET(self):
|
2012-04-12 13:17:55 +02:00
|
|
|
""" Answer every GET request with the upload form """
|
2012-04-07 02:57:06 +02:00
|
|
|
self.sendResponse(200, self.uploadPage)
|
2012-04-05 15:56:28 +02:00
|
|
|
|
|
|
|
def do_POST(self):
|
2012-04-12 13:17:55 +02:00
|
|
|
""" 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> .
|
|
|
|
"""
|
2012-04-16 18:40:28 +02:00
|
|
|
length = self.getContentLength()
|
|
|
|
if length < 0:
|
|
|
|
return
|
|
|
|
ctype = self.headers.getheader('Content-Type')
|
2012-04-12 13:17:55 +02:00
|
|
|
|
|
|
|
# check for multipart/form-data.
|
2012-04-07 02:57:06 +02:00
|
|
|
if not (ctype and ctype.lower().startswith("multipart/form-data")):
|
2012-04-07 22:34:34 +02:00
|
|
|
# not a normal multipart request ==> handle as PUT request
|
2012-04-12 13:17:55 +02:00
|
|
|
return self.do_PUT(fromPost=True)
|
2012-04-07 02:57:06 +02:00
|
|
|
|
2012-04-12 13:17:55 +02:00
|
|
|
# create FieldStorage object for multipart parsing
|
|
|
|
env = os.environ
|
|
|
|
env['REQUEST_METHOD'] = "POST"
|
|
|
|
fstorage = cgi.FieldStorage(fp=self.rfile, headers=self.headers, environ=env)
|
2012-04-07 02:57:06 +02:00
|
|
|
if not "file" in fstorage:
|
|
|
|
self.sendResponse(400, "No file found in request.")
|
|
|
|
return
|
2012-04-12 13:17:55 +02:00
|
|
|
|
2012-04-07 18:48:48 +02:00
|
|
|
destFileName = self.getTargetName(fstorage["file"].filename)
|
|
|
|
if destFileName == "":
|
2012-04-12 13:17:55 +02:00
|
|
|
self.sendResponse(400, "Filename was empty or invalid")
|
2012-04-07 02:57:06 +02:00
|
|
|
return
|
2012-04-12 13:17:55 +02:00
|
|
|
|
2012-06-26 02:13:24 +02:00
|
|
|
# write file down to disk, send a 200 afterwards
|
2012-04-07 02:57:06 +02:00
|
|
|
target = open(destFileName, "w")
|
2012-06-25 03:37:42 +02:00
|
|
|
bytesLeft = length
|
|
|
|
while bytesLeft > 0:
|
2012-06-26 02:13:24 +02:00
|
|
|
bytesToRead = min(self.blockSize, bytesLeft)
|
2012-06-25 03:37:42 +02:00
|
|
|
target.write(fstorage["file"].file.read(bytesToRead))
|
|
|
|
bytesLeft -= bytesToRead
|
2012-04-07 02:57:06 +02:00
|
|
|
target.close()
|
2012-05-04 13:26:25 +02:00
|
|
|
self.sendResponse(200, "OK! Thanks for uploading")
|
2012-06-25 19:23:50 +02:00
|
|
|
print("Received file '%s' from %s." % (destFileName, self.client_address[0]))
|
2012-04-07 02:57:06 +02:00
|
|
|
|
2012-04-12 13:17:55 +02:00
|
|
|
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> .
|
|
|
|
"""
|
2012-04-16 18:40:28 +02:00
|
|
|
length = self.getContentLength()
|
|
|
|
if length < 0:
|
2012-04-07 18:50:24 +02:00
|
|
|
return
|
|
|
|
|
|
|
|
fileName = urllib.unquote(self.path)
|
|
|
|
if fileName == "/":
|
2012-04-12 13:17:55 +02:00
|
|
|
# if no filename was given we have to generate one
|
2012-04-07 18:50:24 +02:00
|
|
|
fileName = str(time.time())
|
2012-04-12 13:17:55 +02:00
|
|
|
|
2012-04-07 18:50:24 +02:00
|
|
|
cleanFileName = self.getTargetName(fileName)
|
|
|
|
if cleanFileName == "":
|
|
|
|
self.sendResponse(400, "Filename was invalid")
|
|
|
|
return
|
2012-04-12 13:17:55 +02:00
|
|
|
|
|
|
|
# Sometimes clients want to be told to continue with their transfer
|
2012-04-07 18:50:24 +02:00
|
|
|
if self.headers.getheader("Expect") == "100-continue":
|
|
|
|
self.send_response(100)
|
|
|
|
self.end_headers()
|
2012-04-12 13:17:55 +02:00
|
|
|
|
2012-04-07 18:50:24 +02:00
|
|
|
target = open(cleanFileName, "w")
|
2012-06-25 03:37:42 +02:00
|
|
|
bytesLeft = int(self.headers['Content-Length'])
|
|
|
|
while bytesLeft > 0:
|
2012-06-26 02:13:24 +02:00
|
|
|
bytesToRead = min(self.blockSize, bytesLeft)
|
2012-06-25 03:37:42 +02:00
|
|
|
target.write(self.rfile.read(bytesToRead))
|
|
|
|
bytesLeft -= bytesToRead
|
2012-04-07 18:50:24 +02:00
|
|
|
target.close()
|
2012-06-26 02:13:24 +02:00
|
|
|
self.sendResponse(200 if fromPost else 201, "OK!")
|
2012-04-07 18:50:24 +02:00
|
|
|
|
2012-04-16 18:40:28 +02:00
|
|
|
def getContentLength(self):
|
|
|
|
length = 0
|
|
|
|
try:
|
|
|
|
length = int(self.headers['Content-Length'])
|
|
|
|
except (ValueError, KeyError):
|
|
|
|
pass
|
|
|
|
if length <= 0:
|
2012-04-20 04:23:23 +02:00
|
|
|
self.sendResponse(411, "Content-Length was invalid or not set.")
|
2012-04-16 18:40:28 +02:00
|
|
|
return -1
|
2012-04-26 14:52:25 +02:00
|
|
|
if self.maxUploadSize > 0 and length > self.maxUploadSize:
|
2012-04-16 18:40:28 +02:00
|
|
|
self.sendResponse(413, "Your file was too big! Maximum allowed size is %d byte. <a href=\"/\">back</a>" % self.maxUploadSize)
|
|
|
|
return -1
|
|
|
|
return length
|
|
|
|
|
2012-04-07 02:57:06 +02:00
|
|
|
def sendResponse(self, code, msg):
|
2012-05-04 13:17:40 +02:00
|
|
|
""" Send a HTTP response with HTTP statuscode code and message msg,
|
|
|
|
providing the correct content-length.
|
2012-04-12 13:17:55 +02:00
|
|
|
"""
|
2012-04-07 02:57:06 +02:00
|
|
|
self.send_response(code)
|
|
|
|
self.send_header('Content-Type', 'text/html')
|
2012-04-13 20:17:48 +02:00
|
|
|
self.send_header('Content-Length', str(len(msg)))
|
2012-04-05 15:56:28 +02:00
|
|
|
self.end_headers()
|
2012-04-07 02:57:06 +02:00
|
|
|
self.wfile.write(msg)
|
2012-04-07 18:50:24 +02:00
|
|
|
|
2012-04-07 18:48:48 +02:00
|
|
|
def getTargetName(self, fname):
|
2012-04-12 13:17:55 +02:00
|
|
|
""" Generate a clean and secure filename.
|
|
|
|
|
|
|
|
This function takes a filename and strips all the slashes out of it.
|
2012-04-28 17:52:57 +02:00
|
|
|
If the file already exists in the target directory, a (NUM) will be
|
2012-04-12 13:17:55 +02:00
|
|
|
appended, so no file will be overwritten.
|
|
|
|
"""
|
2012-04-07 18:48:48 +02:00
|
|
|
cleanFileName = fname.replace("/", "")
|
|
|
|
if cleanFileName == "":
|
|
|
|
return ""
|
2012-06-26 02:13:24 +02:00
|
|
|
destFileName = os.path.join(self.targetDir, cleanFileName)
|
2012-04-07 18:48:48 +02:00
|
|
|
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
|
2012-04-07 02:57:06 +02:00
|
|
|
|
|
|
|
class ThreadedHTTPServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
|
2012-04-05 15:56:28 +02:00
|
|
|
pass
|
|
|
|
|
2012-04-14 22:31:09 +02:00
|
|
|
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)
|
2012-06-25 19:23:50 +02:00
|
|
|
except SSL.Error as e:
|
2012-04-14 22:31:09 +02:00
|
|
|
if str(e) == "":
|
2012-06-26 02:13:24 +02:00
|
|
|
print("%s SSL error (empty error message)" % (self.client_address[0],))
|
2012-04-14 22:31:09 +02:00
|
|
|
else:
|
2012-06-26 02:13:24 +02:00
|
|
|
print("%s SSL error: %s" % (self.client_address[0], e))
|
2012-04-14 22:31:09 +02:00
|
|
|
return X
|
|
|
|
|
2012-06-19 18:24:22 +02:00
|
|
|
|
2012-04-14 22:31:09 +02:00
|
|
|
class SecureThreadedHTTPServer(ThreadedHTTPServer):
|
2012-06-25 02:13:36 +02:00
|
|
|
def __init__(self, pubKey, privKey, server_address, RequestHandlerClass, bind_and_activate=True):
|
|
|
|
ThreadedHTTPServer.__init__(self, server_address, RequestHandlerClass, bind_and_activate)
|
2012-04-14 22:31:09 +02:00
|
|
|
ctx = SSL.Context(SSL.SSLv23_METHOD)
|
2012-06-26 02:13:24 +02:00
|
|
|
if type(pubKey) is crypto.X509 and type(privKey) is crypto.PKey:
|
2012-04-16 14:51:12 +02:00
|
|
|
ctx.use_certificate(pubKey)
|
|
|
|
ctx.use_privatekey(privKey)
|
|
|
|
else:
|
|
|
|
ctx.use_certificate_file(pubKey)
|
|
|
|
ctx.use_privatekey_file(privKey)
|
2012-04-14 22:31:09 +02:00
|
|
|
|
|
|
|
self.bsocket = socket.socket(self.address_family, self.socket_type)
|
|
|
|
self.socket = SSL.Connection(ctx, self.bsocket)
|
|
|
|
|
2012-06-25 02:13:36 +02:00
|
|
|
if bind_and_activate:
|
|
|
|
self.server_bind()
|
|
|
|
self.server_activate()
|
2012-04-14 22:31:09 +02:00
|
|
|
|
|
|
|
def shutdown_request(self, request):
|
|
|
|
request.shutdown()
|
|
|
|
|
2012-06-19 18:24:22 +02:00
|
|
|
|
2012-04-14 22:31:09 +02:00
|
|
|
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)
|
|
|
|
|
2012-04-13 20:17:48 +02:00
|
|
|
class ServeFileException(Exception):
|
|
|
|
pass
|
|
|
|
|
2012-06-19 18:24:22 +02:00
|
|
|
|
2012-04-13 20:17:48 +02:00
|
|
|
class ServeFile():
|
|
|
|
""" Main class to manage everything. """
|
|
|
|
|
2012-04-27 15:37:35 +02:00
|
|
|
_NUM_MODES = 4
|
|
|
|
(MODE_SINGLE, MODE_SINGLETAR, MODE_UPLOAD, MODE_LISTDIR) = range(_NUM_MODES)
|
2012-04-13 20:17:48 +02:00
|
|
|
|
2012-04-14 22:31:09 +02:00
|
|
|
def __init__(self, target, port=8080, serveMode=0, useSSL=False):
|
2012-04-13 20:17:48 +02:00
|
|
|
self.target = target
|
|
|
|
self.port = port
|
|
|
|
self.serveMode = serveMode
|
|
|
|
self.dirCreated = False
|
2012-04-14 22:31:09 +02:00
|
|
|
self.useSSL = useSSL
|
2012-04-16 14:51:12 +02:00
|
|
|
self.cert = self.key = None
|
2012-04-16 16:36:40 +02:00
|
|
|
self.auth = None
|
2012-04-16 18:40:28 +02:00
|
|
|
self.maxUploadSize = 0
|
2012-06-25 02:13:36 +02:00
|
|
|
self.listenIPv4 = True
|
|
|
|
self.listenIPv6 = True
|
2012-04-13 20:17:48 +02:00
|
|
|
|
2012-04-27 15:37:35 +02:00
|
|
|
if self.serveMode not in range(self._NUM_MODES):
|
2012-04-13 20:17:48 +02:00
|
|
|
self.serveMode = None
|
2012-05-04 13:17:40 +02:00
|
|
|
raise ValueError("Unknown serve mode, needs to be MODE_SINGLE, MODE_SINGLETAR, MODE_UPLOAD or MODE_DIRLIST.")
|
2012-04-13 20:17:48 +02:00
|
|
|
|
2012-06-25 02:13:36 +02:00
|
|
|
def setIPv4(self, ipv4):
|
|
|
|
""" En- or disable ipv4 """
|
|
|
|
self.listenIPv4 = ipv4
|
|
|
|
|
|
|
|
def setIPv6(self, ipv6):
|
|
|
|
""" En- or disable ipv6 """
|
|
|
|
self.listenIPv6 = ipv6
|
|
|
|
|
2012-04-13 20:17:48 +02:00
|
|
|
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|" + \
|
2012-06-27 01:44:34 +02:00
|
|
|
"sed -n -e 's/.*inet6\{0,1\} \([0-9.a-fA-F:]\+\).*/\\1/ p'|" + \
|
2012-04-16 16:42:17 +02:00
|
|
|
"grep -v '^fe80\|^127.0.0.1\|^::1'", \
|
|
|
|
shell=True, stdout=PIPE, stderr=PIPE)
|
2012-04-13 20:17:48 +02:00
|
|
|
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|" + \
|
2012-06-27 01:44:34 +02:00
|
|
|
"sed -n 's/.*inet6\{0,1\}\( addr:\)\{0,1\} \{0,1\}\([0-9a-fA-F.:]*\).*/" + \
|
|
|
|
"\\2/p'|" + \
|
2012-04-13 20:17:48 +02:00
|
|
|
"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:
|
2012-04-14 22:31:09 +02:00
|
|
|
# we couldn't find any ip address
|
2012-04-13 20:17:48 +02:00
|
|
|
proc = None
|
|
|
|
if proc:
|
|
|
|
ips = proc.stdout.read().strip().split("\n")
|
2012-06-25 02:13:36 +02:00
|
|
|
|
|
|
|
# filter out ips we are not listening on
|
|
|
|
if not self.listenIPv6:
|
2012-06-26 02:13:24 +02:00
|
|
|
ips = filter(lambda ip: ":" not in ip, ips)
|
2012-06-25 02:13:36 +02:00
|
|
|
if not self.listenIPv4:
|
2012-06-26 02:13:24 +02:00
|
|
|
ips = filter(lambda ip: "." not in ip, ips)
|
2012-06-25 02:13:36 +02:00
|
|
|
|
2012-04-13 20:17:48 +02:00
|
|
|
return ips
|
|
|
|
return None
|
2012-04-14 22:31:09 +02:00
|
|
|
|
2012-04-16 14:51:12 +02:00
|
|
|
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
|
|
|
|
|
2012-04-16 18:40:28 +02:00
|
|
|
def setMaxUploadSize(self, limit):
|
|
|
|
""" Set the maximum upload size in byte """
|
|
|
|
self.maxUploadSize = limit
|
|
|
|
|
2012-04-27 03:52:07 +02:00
|
|
|
def setCompression(self, compression):
|
|
|
|
""" Set the compression of TarFileHandler """
|
|
|
|
if self.serveMode != self.MODE_SINGLETAR:
|
2012-05-04 13:17:40 +02:00
|
|
|
raise ServeFileException("Compression mode can only be set in tar-mode.")
|
2012-04-27 03:52:07 +02:00
|
|
|
if compression not in TarFileHandler.compressionMethods:
|
2012-05-04 13:17:40 +02:00
|
|
|
raise ServeFileException("Compression mode not available.")
|
2012-04-27 03:52:07 +02:00
|
|
|
TarFileHandler.compression = compression
|
|
|
|
|
2012-04-16 14:51:12 +02:00
|
|
|
def genKeyPair(self):
|
2012-06-25 19:23:50 +02:00
|
|
|
print("Generating SSL certificate...", end="")
|
2012-04-16 16:36:31 +02:00
|
|
|
sys.stdout.flush()
|
|
|
|
|
2012-04-16 14:51:12 +02:00
|
|
|
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 = []
|
2012-06-25 02:13:36 +02:00
|
|
|
for ip in self.getIPs() + ["127.0.0.1", "::1"]:
|
2012-04-16 14:51:12 +02:00
|
|
|
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
|
2012-04-16 16:36:31 +02:00
|
|
|
|
2012-06-25 19:23:50 +02:00
|
|
|
print("done.")
|
|
|
|
print("SHA1 fingerprint:", cert.digest("sha1"))
|
|
|
|
print("MD5 fingerprint:", cert.digest("md5"))
|
2012-04-28 17:52:57 +02:00
|
|
|
|
2012-04-14 22:31:09 +02:00
|
|
|
def _getCert(self):
|
2012-04-16 14:51:12 +02:00
|
|
|
return self.cert
|
2012-04-14 22:31:09 +02:00
|
|
|
|
|
|
|
def _getKey(self):
|
2012-04-16 14:51:12 +02:00
|
|
|
return self.key
|
2012-04-28 17:52:57 +02:00
|
|
|
|
2012-06-25 12:39:19 +02:00
|
|
|
def setAuth(self, user, password, realm=None):
|
2012-06-26 02:13:24 +02:00
|
|
|
if not user or not password:
|
2012-05-04 13:17:40 +02:00
|
|
|
raise ServeFileException("User and password both need to be at least one character.")
|
2012-04-16 16:36:40 +02:00
|
|
|
self.auth = base64.b64encode("%s:%s" % (user, password))
|
2012-06-25 12:39:19 +02:00
|
|
|
self.authrealm = realm
|
2012-04-16 16:36:40 +02:00
|
|
|
|
2012-06-25 02:13:36 +02:00
|
|
|
def _createServer(self, handler, withv6=False):
|
|
|
|
ThreadedHTTPServer.address_family = socket.AF_INET
|
|
|
|
SecureThreadedHTTPServer.address_family = socket.AF_INET
|
|
|
|
listenIp = ''
|
2012-04-14 22:31:09 +02:00
|
|
|
server = None
|
2012-06-25 02:13:36 +02:00
|
|
|
|
|
|
|
if withv6:
|
|
|
|
listenIp = '::'
|
|
|
|
ThreadedHTTPServer.address_family = socket.AF_INET6
|
|
|
|
SecureThreadedHTTPServer.address_family = socket.AF_INET6
|
|
|
|
|
2012-04-14 22:31:09 +02:00
|
|
|
if self.useSSL:
|
2012-04-16 14:51:12 +02:00
|
|
|
if not self._getKey():
|
|
|
|
self.genKeyPair()
|
2012-06-25 20:33:21 +02:00
|
|
|
try:
|
2012-06-26 02:19:04 +02:00
|
|
|
server = SecureThreadedHTTPServer(self._getCert(), self._getKey(),
|
|
|
|
(listenIp, self.port), handler, bind_and_activate=False)
|
2012-06-25 20:33:21 +02:00
|
|
|
except SSL.Error as e:
|
2012-06-26 02:19:04 +02:00
|
|
|
raise ServeFileException("SSL error: Could not read SSL public/private key from file(s) (error was: \"%s\")" % (e[0][0][2],))
|
2012-04-14 22:31:09 +02:00
|
|
|
else:
|
2012-06-26 02:19:04 +02:00
|
|
|
server = ThreadedHTTPServer((listenIp, self.port), handler,
|
|
|
|
bind_and_activate=False)
|
2012-06-25 02:13:36 +02:00
|
|
|
|
|
|
|
if withv6:
|
|
|
|
server.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
|
|
|
|
|
|
|
|
server.server_bind()
|
|
|
|
server.server_activate()
|
|
|
|
|
2012-04-14 22:31:09 +02:00
|
|
|
return server
|
|
|
|
|
2012-04-13 20:17:48 +02:00
|
|
|
def serve(self):
|
|
|
|
self.handler = self._confAndFindHandler()
|
2012-06-25 02:13:36 +02:00
|
|
|
self.server = []
|
|
|
|
|
2012-06-25 20:03:34 +02:00
|
|
|
try:
|
|