Forked from UniIsland/SimpleHTTPServerWithUpload.py
Last active
November 18, 2024 00:26
-
-
Save swerder/f1ef7cfb48f45370cd75fea47593de74 to your computer and use it in GitHub Desktop.
Simple Python Http Server with Upload
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
"""HTTP Server with auth and Upload. | |
This module builds on SimpleHTTPRequestHandler, | |
implements simple Authentication, use ssl | |
full "multipart" rfc2046 handling, | |
multi file upload, create folder and delete files, | |
TableView file listing with icon/size/date | |
from bones7456: https://github.com/bones7456/bones7456/blob/master/SimpleHTTPServerWithUpload.py / https://gist.github.com/UniIsland/3346170 | |
- originally based logic | |
from cpdef: https://gist.github.com/cpdef/9f4fa956ff41ca8b9902c4b329596acc | |
- authenticate, certificate, python3 | |
from joonahn: https://gist.github.com/joonahn/5c21fde633bf61087fa3faea77e1f77f | |
- multi upload | |
from Tallguy297: https://github.com/Tallguy297/SimpleHTTPServerWithUpload/blob/master/SimpleHTTPServerWithUpload.py | |
- style change | |
from swerder: https://gist.github.com/swerder/f1ef7cfb48f45370cd75fea47593de74 | |
- merging all together | |
- extend SimpleHTTPRequestHandler not BaseHTTPRequestHandler (getting if-modified support) | |
- refactor html generation | |
- complete refactor | |
- add full "multipart/form-data" handling | |
- add folder creation | |
- add file deletion | |
create certificate with the following command: | |
openssl req -new -x509 -keyout server.pem -out server.pem -days 365 -nodes | |
usage: server.py [port] [user:password] [certfile] | |
example: ../server.py 8443 test:topsecret ../server.pem | |
access the server via the browser: | |
https://ip:port | |
the "s" in https is important for ssl/tls | |
""" | |
__version__ = "0.3" | |
__all__ = ["AuthRequestHandler", "MultipartAuthRequestHandler","FileUploadRequestHandler","FileUploadRequestHandlerTableView"] | |
__author__ = "swerder" | |
__source_page__ = "https://gist.github.com/swerder/f1ef7cfb48f45370cd75fea47593de74" | |
__contributors__ = "bones7456,cpdef,joonahn,Tallguy297" | |
import os | |
import posixpath | |
import http.server | |
import urllib.request, urllib.parse, urllib.error | |
import html | |
import http | |
import shutil | |
import mimetypes | |
import re | |
from io import BytesIO | |
import ssl | |
import sys | |
import base64 | |
import time | |
from string import Template | |
class AuthRequestHandler(http.server.SimpleHTTPRequestHandler): | |
"""Handler for HEAD,GET,POST based on SimpleHTTPRequestHandler that handle Authentication | |
after success it call do_<VERB>_auth (e.g. do_GET_auth) | |
""" | |
server_version = "AuthRequestHandler/" + __version__ | |
sys_version = "" # replaces Python/3.X.X | |
sleep_after_login_failed = 10 | |
realm = "Login" | |
def is_authenticated(self): | |
global key | |
auth_header = self.headers['Authorization'] | |
if auth_header != None: | |
auth_header = auth_header.encode("utf-8") | |
return auth_header and auth_header == b'Basic ' + key | |
def do_AUTHHEAD(self): | |
self.send_response(http.HTTPStatus.UNAUTHORIZED) | |
self.send_header('WWW-Authenticate', f'Basic realm="{self.realm}"') | |
self.send_header('Content-type', 'text/html') | |
self.end_headers() | |
def try_authenticate(self): | |
if not self.is_authenticated(): | |
self.server_version = "" # don't show what I am before login | |
self.do_AUTHHEAD() | |
print('not authenticated') | |
self.wfile.write(b'not authenticated') | |
self.close_connection = True | |
time.sleep(sleep_after_login_failed) | |
return False | |
print('authenticated') | |
return True | |
def do_HEAD_auth(self): | |
super().do_HEAD() | |
def do_GET_auth(self): | |
super().do_GET() | |
def do_POST_auth(self): | |
super().do_POST() | |
def do_HEAD(self): | |
"""Serve a HEAD request.""" | |
if self.try_authenticate(): | |
self.do_HEAD_auth() | |
def do_GET(self): | |
"""Serve a GET request.""" | |
if self.try_authenticate(): | |
self.do_GET_auth() | |
def do_POST(self): | |
"""Serve a POST request.""" | |
if self.try_authenticate(): | |
self.do_POST_auth() | |
class PartConf(): | |
def __init__(self, name, out=None, headers=None, handler=None): | |
self.name = name | |
self.out = out | |
self.headers = headers | |
self.handler = handler | |
class DisplayableException(Exception): | |
pass | |
class MultipartAuthRequestHandler(AuthRequestHandler): | |
"""Handler parse POST request with content-type 'multipart/*' designed for 'multipart/form-data' | |
""" | |
server_version = "MultipartAuthRequestHandler/" + __version__ | |
def generate_html_response(self, title, headContent, bodyContent, lang="en", enc="UTF-8"): | |
return f""" | |
<!DOCTYPE html> | |
<html lang="{lang}"> | |
<head> | |
<meta charset="{enc}"> | |
<title>{title}</title> | |
{headContent} | |
</head> | |
<body> | |
{bodyContent} | |
</body> | |
</html> | |
""".encode(enc, 'surrogateescape') | |
def send_html_response_header(self, htmlContent, status=http.HTTPStatus.OK, enc="UTF-8"): | |
self.send_response(status) | |
self.send_header("Content-type", f"text/html; charset={enc}") | |
self.send_header("Content-Length", str(len(htmlContent))) | |
self.end_headers() | |
def send_html_response(self, title, headContent, bodyContent, lang="en", enc="UTF-8", status=http.HTTPStatus.OK): | |
htmlContent = self.generate_html_response(title, headContent, bodyContent, lang, enc) | |
self.send_html_response_header(htmlContent, status, enc) | |
self.wfile.write(htmlContent) | |
def do_POST_auth(self): | |
try: | |
info = self.deal_post_data() | |
status = http.HTTPStatus.OK | |
except DisplayableException as ex: | |
status = http.HTTPStatus.INTERNAL_SERVER_ERROR | |
info = str(ex) | |
print((status, info, "by: ", self.client_address, self.headers.get('X-Forwarded-For', ''))) | |
bodyContent = self.get_POST_body_html(info, status) | |
self.send_html_response("post response page", "", bodyContent, status=status) | |
def get_POST_body_html(self, info, status): | |
return info; | |
def deal_post_data(self): | |
if self.headers.get_content_maintype() == "multipart": | |
return self.deal_multipart_data() | |
raise DisplayableException("multipart Content-Type required, e.g. multipart/form-data but is " + self.headers.get_content_type()) | |
def deal_multipart_data(self): | |
boundary = self.headers.get_boundary() | |
if boundary is None: | |
raise DisplayableException("Content-Type header doesn't contain boundary") | |
boundary = boundary.encode() | |
boundaryEnd = boundary + b"--" | |
remainbytes = int(self.headers['content-length']) | |
response = [] | |
#read all parts, https://www.rfc-editor.org/rfc/rfc2046#section-5.1.1 | |
partConf = PartConf("preamble") | |
preline = None | |
while remainbytes > 0: | |
line = self.rfile.readline() | |
remainbytes -= len(line) | |
#print (partConf.name, "line", line, "preline",preline, remainbytes) # debuging | |
if boundary in line: | |
if partConf.out != None: | |
#write last part/preline but remove CRLF (part of boundary logic) | |
if preline[-2:] == b'\r\n': #CRLF | |
preline = preline[0:-2] | |
else: #only CR or only LF (not correct boundary logic) | |
preline = preline[0:-1] | |
partConf.out.write(preline) | |
if partConf.handler: | |
message = partConf.handler(partConf) | |
if message != None: | |
response.append(message) | |
else: | |
if partConf.out: | |
partConf.out.close() | |
if boundaryEnd in line: | |
partConf = PartConf("epilogue") | |
else: | |
partConf = PartConf("__header__", None, {}) | |
preline = None | |
else: | |
if partConf.out != None: | |
partConf.out.write(preline) | |
elif partConf.name == "__header__": | |
#read part header | |
if preline != None: | |
if preline == b'\r\n' or preline == b'\n': | |
partConf = self.deal_multipart_header(partConf) | |
else: | |
header = [p.strip() for p in preline.decode().split(":", 1)] | |
partConf.headers[header[0]] = header[-1] #-1 =last, here same as 1, but no error if not correct header/no':' delimiter | |
preline = line | |
else: | |
if partConf.name != "epilogue": | |
raise DisplayableException("invalid multipart, end boundary not found") | |
#if partConf.out != None: | |
# partConf.out.write(preline) | |
#if partConf.handler: | |
# message = partConf.handler(partConf) | |
# if message != None: | |
# response.append(message) | |
return "<br>".join(response) | |
def split_header_value(self, header): | |
header_parts = [p.strip() for p in header.split(";")] | |
value = header_parts[0] | |
params = {key.strip():val.strip().strip('"') for key,val in [p.split("=") for p in header_parts[1:]]} | |
return value, params | |
def deal_multipart_header(self, partConf): | |
if self.headers.get_content_type() == "multipart/form-data": | |
#https://www.rfc-editor.org/rfc/rfc7578#section-4.2 | |
if not "Content-Disposition" in partConf.headers: | |
raise DisplayableException("missing Content-Disposition header, invalid multipart/form-data syntax") | |
dispositionValue, dispositionParams = self.split_header_value(partConf.headers["Content-Disposition"]) | |
if dispositionValue != "form-data": | |
raise DisplayableException(f"invalid Content-Disposition header, expect 'form-data' type but get '${dispositionValue}'") | |
name = dispositionParams.get("name") | |
if not name: | |
raise DisplayableException("invalid Content-Disposition header, need name param") | |
return self.deal_field_header(dispositionParams, partConf.headers) | |
else: | |
return PartConf(None, BytesIO(), partConf.headers, self.deal_field_end) | |
def deal_field_header(self, dispositionParams, headers): | |
return PartConf(dispositionParams.get("name"), BytesIO(), headers, self.deal_field_end) | |
def deal_field_end(self, partConf): | |
content = partConf.out.getvalue().decode() | |
partConf.out.close() | |
return self.deal_field_data(partConf.name, content, partConf.headers) | |
def deal_field_data(self, name, content, headers): | |
return f"{name} = '{content}'" | |
class FileUploadRequestHandler(MultipartAuthRequestHandler): | |
"""HTTP request handler with GET/HEAD/POST commands. | |
This serves files from the current directory and any of it's | |
subdirectories. The MIME type for files is determined by | |
calling the .guess_type() method. And can reveive file upload, | |
folder creation and file delete by client. | |
The GET/HEAD requests are identical except that the HEAD | |
request omits the actual contents of the file. | |
""" | |
server_version = "FileUploadRequestHandler/" + __version__ | |
show_upload = True | |
show_create_folder = True | |
show_delete_file = True | |
def get_base_style_html(self): | |
return """<style type="text/css"> | |
* {font-family: Helvetica; font-size: 16px; } | |
a { text-decoration: none; } | |
</style> | |
""" | |
def get_POST_body_html(self, info, status): | |
if 'referer' in self.headers: | |
referer = html.escape(self.headers['referer']) | |
backLink = f'<br><a href="{referer}">back</a>' | |
else: | |
backLink = '' | |
result = "Success:" if status == http.HTTPStatus.OK else "Failed:" | |
bodyContent = Template(""" | |
<h2>Upload Result Page</h2> | |
<hr> | |
<strong>$result</strong><br> | |
$info | |
$backLink | |
<hr> | |
<small>Powered By: $by, check for new version <a href="$src" target="_blank">here</a>.</small> | |
<hr> | |
""").substitute(result=result, info=info, backLink=backLink, by=__author__ + " and " + __contributors__, src=__source_page__).strip() | |
return bodyContent | |
def deal_field_header(self, dispositionParams, headers): | |
if "filename" in dispositionParams: | |
return self.deal_fileupload_header(dispositionParams, headers) | |
return super().deal_field_header(dispositionParams, headers) | |
def deal_fileupload_header(self, dispositionParams, headers): | |
if not self.show_upload: | |
raise DisplayableException("disallowed upload request") | |
filename = dispositionParams.get("filename") | |
if not filename: | |
raise DisplayableException("Can't find out file name...") | |
filePath = self.get_full_path(filename) | |
if filePath == None: | |
raise DisplayableException("upload to folder not allowed") | |
print(f"try to open {filePath}") | |
try: | |
out = open(filePath, 'wb') | |
except IOError: | |
raise DisplayableException("Can't create file to write.<br>Do you have permission to write?") | |
partConf = PartConf("file", out, headers, self.deal_file_end) | |
partConf.dispositionParams = dispositionParams | |
return partConf | |
def get_full_path(self, filename): | |
path = self.translate_path(self.path) | |
#prevent any folder char in filename | |
filename = filename.split('/')[-1].split('\\')[-1] | |
filePath = os.path.join(path, filename) | |
#double check valid path | |
if not os.path.abspath(filePath).startswith(os.path.abspath("."+self.path)): | |
return None | |
return filePath | |
def deal_field_data(self, name, content, headers): | |
if name == "folder": | |
return self.deal_field_folder_data(content, headers) | |
elif name == "delete": | |
return self.deal_field_delete_data(content, headers) | |
else: | |
raise DisplayableException(f"unknown field: {name} = '{content}'") | |
def deal_field_folder_data(self, content, headers): | |
if not self.show_create_folder: | |
raise DisplayableException("disallowed folder request") | |
dirPath = self.get_full_path(content) | |
if not dirPath: | |
raise DisplayableException(f"folder name invalid '{content}'") | |
elif not os.path.exists(dirPath): | |
os.mkdir(dirPath) | |
return f"created folder: '{content}'" | |
else: | |
raise DisplayableException(f"folder '{content}' already exist") | |
def deal_field_delete_data(self, content, headers): | |
if not self.show_delete_file: | |
raise DisplayableException("disallowed delete request") | |
filePath = self.get_full_path(content) | |
if not filePath: | |
raise DisplayableException(f"file name invalid '{content}'") | |
if os.path.exists(filePath): | |
os.remove(filePath) | |
return f"deleted file '{content}'" | |
else: | |
raise DisplayableException(f"file does not exist '{content}'") | |
def deal_file_end(self, partConf): | |
partConf.out.close() | |
return partConf.dispositionParams.get("filename") + " uploaded" | |
def list_directory(self, path): | |
"""Helper to produce a directory listing (absent index.html). | |
Return value is either a file object, or None (indicating an | |
error). In either case, the headers are sent, making the | |
interface the same as for send_head(). | |
""" | |
try: | |
list = os.listdir(path) | |
except OSError: | |
self.send_error( | |
http.HTTPStatus.NOT_FOUND, | |
"No permission to list directory") | |
return None | |
list.sort(key=lambda a: a.lower()) | |
try: | |
displaypath = urllib.parse.unquote(self.path, errors='surrogatepass') | |
except UnicodeDecodeError: | |
displaypath = urllib.parse.unquote(path) | |
displaypath = html.escape(displaypath, quote=False) | |
enc = sys.getfilesystemencoding() | |
htmlContent = self.list_directory_generate_html(path, list, displaypath, enc) | |
self.send_html_response_header(htmlContent, enc=enc) | |
f = BytesIO() | |
f.write(htmlContent) | |
f.seek(0) | |
return f | |
def list_directory_generate_html(self, path, list, displaypath, enc): | |
fileListHtml, headContent = self.list_directory_files(path, list) | |
title = f'Directory listing for {displaypath}' | |
r = [] | |
r.append(f'<h1>{title}</h1>') | |
if self.show_upload: | |
r.append(self.get_upload_html()) | |
if self.show_create_folder: | |
r.append(self.get_create_folder_html()) | |
r.append('<hr>') | |
r.append(fileListHtml) | |
r.append('<hr>') | |
bodyContent = '\n'.join(r) | |
return self.generate_html_response(title, headContent, bodyContent, enc=enc) | |
def get_upload_html(self): | |
return """ | |
<hr> | |
<form ENCTYPE="multipart/form-data" method="post"> | |
<input name="file" type="file" multiple/> | |
<input type="submit" value="upload"/> | |
</form> | |
""" | |
def get_create_folder_html(self): | |
return """ | |
<hr> | |
<form ENCTYPE="multipart/form-data" method="post"> | |
<label for="folder">new folder</label> | |
<input name="folder" id="folder" type="text"/> | |
<input type="submit" value="create"/> | |
</form> | |
""" | |
def get_delete_html(self, filename): | |
filename = html.escape(filename) | |
return f""" | |
<form ENCTYPE="multipart/form-data" method="post" style="display: inline;"> | |
<input name="delete" type="hidden" value="{filename}"/> | |
<input type="submit" value="delete"/> | |
</form> | |
""" | |
def list_directory_files(self, path, list): | |
r = [] | |
r.append('<ul>') | |
if path[:-1] != os.getcwd(): | |
r.append('<li><a href="../">Parent Directory</a></li>') | |
deleteForm = "" | |
for name in list: | |
fullname = os.path.join(path, name) | |
displayname = linkname = name | |
# Append / for directories or @ for symbolic links | |
if os.path.isdir(fullname): | |
displayname = name + "/" | |
linkname = name + "/" | |
deleteForm = "" | |
elif self.show_delete_file: | |
deleteForm = self.get_delete_html(name) | |
if os.path.islink(fullname): | |
displayname = name + "@" | |
# Note: a link to a directory displays with @ and links with / | |
r.append('<li><a href="%s">%s</a>%s</li>' | |
% (urllib.parse.quote(linkname, errors='surrogatepass'), | |
html.escape(displayname, quote=False), deleteForm)) | |
r.append('</ul>') | |
return '\n'.join(r), self.get_base_style_html() | |
def fbytes(size): | |
'Return the given bytes as a human friendly KB, MB, GB, or TB string' | |
size = float(size) | |
KB = float(1024) | |
MB = float(KB ** 2) # 1,048,576 | |
GB = float(KB ** 3) # 1,073,741,824 | |
TB = float(KB ** 4) # 1,099,511,627,776 | |
if size < KB: | |
return '{0} {1}'.format(size,'Bytes' if size > 1 else 'Byte') | |
elif KB <= size < MB: | |
return '{0:.2f} KB'.format(size/KB) | |
elif MB <= size < GB: | |
return '{0:.2f} MB'.format(size/MB) | |
elif GB <= size < TB: | |
return '{0:.2f} GB'.format(size/GB) | |
elif TB <= size: | |
return '{0:.2f} TB'.format(size/TB) | |
class FileUploadRequestHandlerTableView(FileUploadRequestHandler): | |
server_version = "FileUploadRequestHandlerTableView/" + __version__ | |
image_ext = ['bmp','gif','jpg','jpeg','png'] | |
ext_to_class = { | |
'avi':'img-movie', | |
'mpg':'img-movie', | |
'idx':'img-subtitle', | |
'srt':'img-subtitle', | |
'sub':'img-subtitle', | |
'iso':'img-iso', | |
} | |
class_to_img = { | |
'img-default':'data:image/gif;base64,R0lGODlhGAAYAMIAAP///7+/v7u7u1ZWVTc3NwAAAAAAAAAAACH+RFRoaXMgaWNvbiBpcyBpbiB0aGUgcHVibGljIGRvbWFpbi4gMTk5NSBLZXZpbiBIdWdoZXMsIGtldmluaEBlaXQuY29tACH5BAEAAAEALAAAAAAYABgAAANdGLrc/jAuQaulQwYBuv9cFnFfSYoPWXoq2qgrALsTYN+4QOg6veFAG2FIdMCCNgvBiAxWlq8mUseUBqGMoxWArW1xXYXWGv59b+WxNH1GV9vsNvd9jsMhxLw+70gAADs=', | |
'img-parent':'data:image/gif;base64,R0lGODlhGAAYAMIAAP///7+/v7u7u1ZWVTc3NwAAAAAAAAAAACH+RFRoaXMgaWNvbiBpcyBpbiB0aGUgcHVibGljIGRvbWFpbi4gMTk5NSBLZXZpbiBIdWdoZXMsIGtldmluaEBlaXQuY29tACH5BAEAAAEALAAAAAAYABgAAANKGLrc/jBKNgIhM4rLcaZWd33KJnJkdaKZuXqTugYFeSpFTVpLnj86oM/n+DWGyCAuyUQymlDiMtrsUavP6xCizUB3NCW4Ny6bJwkAOw==', | |
'img-dir':'data:image/png;base64,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', | |
'img-link':'data:image/gif;base64,R0lGODlhGAAYAPf/AJaWlpqampubm5ycnJ2dnZ6enp+fn6CgoKGhoaKioqOjo6SkpKWlpaampqioqKmpqaqqqqurq6ysrK2tra6urq+vr7CwsLGxsbKysrOzs7S0tLW1tba2tre3t7i4uLm5ubq6uru7u7y8vL29vb6+vr+/v8LCwsPDw8bGxtDQ0NTU1NXV1dbW1tfX19jY2Nra2tzc3N3d3eDg4OHh4eLi4uPj4+Tk5OXl5efn5+np6erq6uvr6+zs7O7u7u/v7/Dw8PHx8fLy8vPz8/T09PX19fb29vf39/j4+Pr6+vv7+/39/f7+/v///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAGAAYAAAI/wCZCBxIsKDBgwgLrsigwUXChEVGYNBwIYKIJA8LFunwocKGDA8ieMg4kAiHDxRmCGyhIAEKkhtR2iCYYYEAkiNQ3ijYIQGAjDkuVFBJsIcBAhcyttCgoSCQBQcUFMn44gIFEiwE/oAqIAfJIREeQLDAZIeCAwO8IuQRowYSIxQgBFhAoQBatQaFiLCQoQIFCxEMREUwoAEPhEA0dMQwQSwCIEFYpKCR8IfiCjWYgJCr4AhJyx13CFRhQYECGBmRcKwgmmAEBCsyltBQQUfBGwUG4MjoYMOIgjsSIJBAskGGEAR3IEhw4AdJExIeyBCIY/kBHySZLNEwgcGGDQYQNBbPLpAIBgULEhB4AIQ8wRMFBIhQ4j4gADs=', | |
'img-movie':'data:image/gif;base64,R0lGODlhGAAYAMIAAP///7+/v7u7u1ZWVTc3NwAAAAAAAAAAACH+RFRoaXMgaWNvbiBpcyBpbiB0aGUgcHVibGljIGRvbWFpbi4gMTk5NSBLZXZpbiBIdWdoZXMsIGtldmluaEBlaXQuY29tACH5BAEAAAEALAAAAAAYABgAAANvGLrc/jAuQqu99BEh8OXE4GzdYJ4mQIZjNXAwp7oj+MbyKjY6ntstyg03E9ZKPoEKyLMll6UgAUCtVi07xspTYWptqBOUxXM9scfQ2Ttx+sbZifmNbiLpbEUPHy1TrIB1Xx1cFHkBW4VODmGNjQ4JADs=', | |
'img-subtitle':'data:image/gif;base64,R0lGODlhGAAYAPf/AAAbDiAfDSoqHjQlADs+J3sxJ0BALERHMk5LN1pSPUZHRk9NQU1OR05ZUFBRRFVXTVdYTVtVQFlVRFtbSF5bTVZWUltbUFlZVFtcVlxdWl5eWl1gWmBiV2FiXWNhXGRjXWlpYmtqZ2xtZmxsaG5ubHJva3Jzb3J0bHN0b3Z5dHd8eXh4dX18dXx/dnt8e31+eahMP4JdWIdnZox4cpVoYKJkXrxqablwcNA6Otg7O/8AAPwHB/0GBvgODvsMDPMbG/ceHvkSEuYqKvYqKvU2NvM6OvQ6OsFQUNVbW8N4eNd0duNeXu9aWvFUVOVqau1jY+1mZu5mZuh5eYSFgIWGgYaFgIiGgYqKiY6LioyMiY2NiYyMio2OiI6OjZCSjZSQi5OSkJGUkpSVk5WVlJWVlZiZlpiZmJ6emp2dnZSko6GhnKGhoKOjoaKnoqSkpKSmpKWmpaampqqrqKurqKqrqq2sqq6urrSyrrCwsLa3tb63tbi4t7q6ur+/vsCbm96Li9iUlMqursmwr9KwsN69veSCguiMjOiOjuaQkOWVleaXmOGfn+aYmOaamuebm+adneecnOeenuiQkOWgoOWsrOasrOWxseW2tue3t+e8vOi+vsHBwcfHx8jIxs7Mx8nJyMvLy8zMy83NzM7Pzs/Pz9PR0dTU1NXV1dXW1tbW1tfX19bb29jY2NnZ2djb29ra2tvb2tvb29za2tzc3N3d3d7e3t/f3+bCwufDw+fGxuTJyejCwujHx+nHx+vPz+rT0+rU1OrX1+vX1+rY2Ojf3+Hh4eLi4uPj4+Hn5+Tk5OXl5ebm5ufn5+nn5+jo6Onp6erp6evr6+zs7PDn5/Dq6vDt7fHv7/Lu7vPu7vPv7/Dw8PHx8fLy8vPz8/Tx8fbz8/T09PX19fX39/b29vj39/j4+Pn4+Pn5+fr5+fr6+vv7+/z7+/z8/P39/f7+/v/+/v///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAGAAYAAAI/wDhCRxIsKDBgwgTZis00F27hAbRNSpSaSC7ZM6ePQsXjdmzZekKUpOio0lBVShjJXvYjp07gsEm/dLhqKC2aNG2LesES13BXJTg4dLBi2C7kPDSyQFTRY26c8akZbolkJGOaQTZFTO2LA8KCCA+zDFTp4aggVCCmCtYrJUxNiI4nAjh4o2HGW1CrhvCxGC5cOVqvcCgQQUWGRtanOkG75qOQwXbhWvZ7lOZMIGSsJjihY5AYTowFWRnipUtT6BGWIARY8IXPc1eXtIxrKC7cqJCjdJCIcIBAwRoaLqU6IkRHthGnwPzoEGKDBISMHDAZWAvHUTWjaa1J42NAgAELMBAMGCNwHbWekQxqA4VMkBKSokZE8AKtHLpjuHxo0OSQXfuLKJIOHdc0IECFaxgQivpxHGDDpYc9McS5oBTAhxUbNHFFX2I4Q4fR+iwi0GPCCGLK6rYgQYJWZDhBil7cMMJDjpAAg85L8ETCRDVqAONMuGMA0446rDjEzzi5KDDD4j48g48huxAyCqtODNZOeWcM85DAxEDzDcE+YDEK5u0EostbZ2SyizsQASPE4PAo4466TxVZzptuqmLN266GRAAOw==', | |
'img-iso':'data:image/png;base64,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', | |
} | |
def list_directory_head_html_additional(self, usedClass): | |
img_class_templ = Template(".file-img.$clazz { background-image: url('$img')}") | |
return """ | |
<style type="text/css"> | |
* {font-family: Helvetica; font-size: 16px; } | |
a { text-decoration: none; } | |
a:link { text-decoration: none; font-weight: bold; color: #0000ff; } | |
a:visited { text-decoration: none; font-weight: bold; color: #0000ff; } | |
a:active { text-decoration: none; font-weight: bold; color: #0000ff; } | |
a:hover { text-decoration: none; font-weight: bold; color: #ff0000; } | |
table { border-collapse: separate;} | |
th, td { padding:0px 10px;} | |
.file-size { text-align:right; font-weight: bold; color:#FF0000; } | |
.file-date { text-align:right; font-weight: bold; } | |
.file-img { width:24px; height:24px; background-size: contain; background-position: center; background-repeat: no-repeat;} | |
""" + \ | |
"\n".join(img_class_templ.substitute(clazz=clazz, img=img) for clazz,img in self.class_to_img.items() if clazz in usedClass) + \ | |
""" | |
</style> | |
""" | |
def list_directory_files(self, path, list): | |
file_html_templ = Template('<tr><td><div class="file-img $fileImgClass" $specialImgStyle></div></td><td><a href="$linkname" target="$target">$displayname</a></td><td class="file-size">$fsize</td><td class="file-date">$created_date</td>$deleteForm</tr>\n') | |
img_style_templ = Template(''' style="background-image: url('$name')"''') | |
r=[] | |
deleteForm = '' | |
r.append('<table>') | |
usedClass=set() | |
if path[:-1] != os.getcwd(): | |
fileImgClass = 'img-parent' | |
r.append(file_html_templ.substitute(fileImgClass=fileImgClass, specialImgStyle="", linkname="../", target="_self", displayname="Parent Directory", fsize="", created_date="", deleteForm=deleteForm)) | |
usedClass.add(fileImgClass) | |
for name in list: | |
fileImgClass = 'img-default' | |
target = '_blank' | |
specialImgStyle = "" | |
fullname = os.path.join(path, name) | |
displayname = linkname = name | |
fsize = fbytes(os.path.getsize(fullname)) | |
created_date = time.strftime("%d.%m.%Y %H:%M", time.localtime(os.path.getctime(fullname))) | |
# Append / for directories or @ for symbolic links | |
if os.path.isdir(fullname): | |
fileImgClass = 'img-dir' | |
target = '_self' | |
displayname = name + '/' | |
linkname = name + '/' | |
fsize = '' | |
created_date = '' | |
deleteForm = '' | |
elif self.show_delete_file: | |
deleteForm = '<td>' + self.get_delete_html(name) + '</td>' | |
if os.path.islink(fullname): | |
fileImgClass = 'img-link' | |
displayname = name + '@' | |
ext = name.rsplit(".",1)[-1] | |
if ext in self.image_ext: | |
fileImgClass = '' | |
specialImgStyle = img_style_templ.substitute(name=name) | |
else: | |
fileImgClass = self.ext_to_class.get(ext, fileImgClass) | |
# Note: a link to a directory displays with @ and links with / | |
r.append(file_html_templ.substitute(fileImgClass=fileImgClass, specialImgStyle=specialImgStyle, linkname=urllib.parse.quote(linkname), target=target, displayname=html.escape(displayname), fsize=fsize, created_date=created_date, deleteForm=deleteForm)) | |
usedClass.add(fileImgClass) | |
r.append('</table>') | |
return '\n'.join(r), self.list_directory_head_html_additional(usedClass) | |
def run(HandlerClass = FileUploadRequestHandlerTableView, | |
ServerClass = http.server.HTTPServer, | |
port = 8000, | |
protocol = "HTTP/1.0", | |
bind = ""): | |
""" | |
This runs an HTTP server on port 8000 (or the port argument). | |
""" | |
server_address = (bind, port) | |
HandlerClass.protocol_version = protocol | |
httpd = ServerClass(server_address, HandlerClass) | |
global certfile | |
httpd.socket = ssl.wrap_socket(httpd.socket, certfile=certfile, | |
server_side=True) | |
sa = httpd.socket.getsockname() | |
print("Serving HTTP on", sa[0], "port", sa[1], "...") | |
try: | |
httpd.serve_forever() | |
except KeyboardInterrupt: | |
print("\nKeyboard interrupt received, exiting.") | |
httpd.server_close() | |
sys.exit(0) | |
if __name__ == '__main__': | |
if len(sys.argv) < 3: | |
print(sys.argv[0] + " <port> <user>:<pw> <cert.pem>") | |
sys.exit(1) | |
port = int(sys.argv[1]) | |
key = base64.b64encode(sys.argv[2].encode("utf-8")) | |
certfile = sys.argv[3] | |
run(port=port) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
ssl server may optional by args, disabled by default