"""
Simple HTTP Server to exfil JuiceBox files via POST command.
This module is a python3 version of the file_upload_server.py found on:
https://docs.silabs.com/gecko-os/4/standard/4.2/cmd/apps/http-upload.
The direct reference is:
https://docs.silabs.com/resources/gecko-os/examples/http_upload/file_upload_server.py
"""
import os
import posixpath
import http.server
import urllib.parse
import cgi
import shutil
import socketserver
import socket
import sys
from io import BytesIO
DOMAIN = "0.0.0.0"
PORT = 8000
if len(sys.argv) > 2:
PORT = int(sys.argv[2])
DOMAIN = sys.argv[1]
elif len(sys.argv) > 1:
PORT = int(sys.argv[1])
DEBUG = False
class SimpleHTTPRequestHandler(http.server.BaseHTTPRequestHandler):
"""Simple HTTP request handler with POST command.
Can receive a file uploaded by the client.
"""
server_version = "SimpleHTTPWithUpload/"
def do_GET(self):
"""Serve a GET request."""
infoPage = '''
File Upload Server
File Upload Server
'''
# Encode the infoPage string to bytes before writing to BytesIO
f = BytesIO() # Use BytesIO for binary data
f.write(infoPage.encode('utf-8')) # Encode string to bytes
length = f.tell()
f.seek(0)
self.send_response(200)
self.send_header("Content-type", "text/html")
self.send_header("Content-Length", str(length))
self.end_headers()
if f:
self.copyfile(f, self.wfile)
f.close()
def do_POST(self):
"""Serve a POST request."""
r, info = self.handle_post_data()
print("{}\nfrom host:{} port:{}".format(info, self.client_address[0], self.client_address[1]))
f = BytesIO() # Use BytesIO for binary data
f.write('POST result\n'.encode('utf-8')) # Encode string to bytes
if r:
f.write("Success".encode('utf-8')) # Encode string to bytes
else:
f.write("Failure".encode('utf-8')) # Encode string to bytes
f.write(info.encode('utf-8')) # Encode string to bytes
length = f.tell()
f.seek(0)
self.send_response(200)
self.send_header("Content-type", "text/plain")
self.send_header("Content-Length", str(length))
self.end_headers()
if f:
self.copyfile(f, self.wfile)
f.close()
def handle_post_data(self):
content_type_header = self.headers.get('Content-Type')
if not content_type_header:
return (False, "Content-Type header missing.")
ctype, pdict = cgi.parse_header(content_type_header)
if ctype != 'multipart/form-data':
return (False, "Content-Type must be multipart/form-data.")
if 'boundary' not in pdict:
return (False, "Boundary not found in Content-Type header.")
boundary = pdict['boundary'].encode('utf-8')
remainbytes = int(self.headers['content-length'])
# Read the first boundary line
line = self.rfile.readline()
if DEBUG:
print('line:{}'.format(line))
remainbytes -= len(line)
if not boundary in line:
return (False, "Content does NOT begin with boundary")
# Read the Content-Disposition line
disposition_line_bytes = self.rfile.readline()
if DEBUG:
print('disposition_line_bytes:{}'.format(disposition_line_bytes))
remainbytes -= len(disposition_line_bytes)
# Decode the disposition line to string for cgi.parse_header
try:
disposition_line_str = disposition_line_bytes.decode('utf-8')
_, disp_params = cgi.parse_header(disposition_line_str)
filename_str = disp_params.get('filename')
if not filename_str:
return (False, "Unable to determine file name from Content-Disposition header.")
# The filename might contain path separators, so just take the basename
filename_basename = os.path.basename(filename_str)
path = self.translate_path(self.path)
fn = os.path.join(path, filename_basename)
except Exception as e:
return (False, f"Error parsing Content-Disposition header: {e}")
# Read the Content-Type of the file (e.g., image/jpeg) - usually the next line
line = self.rfile.readline()
if DEBUG:
print('line:{}'.format(line))
remainbytes -= len(line)
# Read the empty line separating headers from content
line = self.rfile.readline()
if DEBUG:
print('line:{}'.format(line))
remainbytes -= len(line)
try:
out = open(fn, 'wb')
except IOError:
return (False, "Unable to create file {} for write.".format(fn))
preline = self.rfile.readline()
if DEBUG:
print('preline:{}'.format(preline))
remainbytes -= len(preline)
while remainbytes > 0:
line = self.rfile.readline()
if DEBUG:
print('line:{}'.format(line))
remainbytes -= len(line)
if boundary in line:
preline = preline[0:-1]
if preline.endswith(b'\r'):
preline = preline[0:-1]
out.write(preline)
out.close()
return (True, "Successful Upload:\nFile:{}".format(fn))
else:
out.write(preline)
preline = line
return (False, "Unexpected end of data.")
def translate_path(self, path):
"""Translate a /-separated PATH to the local filename syntax."""
# abandon query parameters
path = path.split('?', 1)[0]
path = path.split('#', 1)[0]
path = posixpath.normpath(urllib.parse.unquote(path))
words = path.split('/')
words = list(filter(None, words))
path = os.getcwd()
for word in words:
_, word = os.path.splitdrive(word)
_, word = os.path.split(word)
if word in (os.curdir, os.pardir):
continue
path = os.path.join(path, word)
return path
def copyfile(self, source, outputfile):
shutil.copyfileobj(source, outputfile)
def main():
Handler = SimpleHTTPRequestHandler
httpd = socketserver.TCPServer(("", PORT), Handler)
print("file_upload_server.py")
print("You can specify server port as an argument. Default port:8000")
print("Serving at: http://{domain}:{port}".format(domain=DOMAIN or "localhost", port=PORT))
print(": {}".format(socket.gethostbyname(socket.gethostname())))
httpd.serve_forever()
if __name__ == '__main__':
main()