The tiniest file-sharing server. Roughly equivalent to 1e-15 ordinary file-sharing servers.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

189 lines
8.1 KiB

#!/usr/bin/python3
"""
Femtoshare
==========
Ultra simple self-hosted file sharing. All files can be accessed/modified by all users. Don't upload anything secret!
Quickstart: run `./femtoshare.py`, then visit `http://localhost:8000/` in your web browser.
See `./femtoshare.py --help` for usage information. See `README.md` for more documentation.
"""
__author__ = "Anthony Zhang (Uberi)"
__version__ = "1.0.4"
__license__ = "MIT"
from http.server import BaseHTTPRequestHandler, HTTPServer
import cgi
import os
import re
import html
import urllib.parse
import argparse
from datetime import datetime
from shutil import copyfileobj
parser = argparse.ArgumentParser()
parser.add_argument("--port", help="local network port to listen on", type=int, default=8000)
parser.add_argument("--public", help="listen on remote network interfaces (allows other hosts to see the website; otherwise only this host can see it)", action="store_true")
parser.add_argument("--files-dir", help="directory to upload/download files from (prefix with # to specify that the path is relative to the Femtoshare executable)", default="#files")
parser.add_argument("--title", help="specify the name and the title of the app",default="femtoshare")
args = parser.parse_args()
if args.public:
SERVER_ADDRESS = ("0.0.0.0", args.port)
else:
SERVER_ADDRESS = ("127.0.0.1", args.port)
if args.files_dir.startswith("@"):
FILES_DIRECTORY = os.path.join(os.path.dirname(os.path.realpath(__file__)), args.files_dir[1:])
else:
FILES_DIRECTORY = args.files_dir
if args.title == "femtoshare":
title = "femtoshare"
else:
title = args.title
class FemtoshareRequestHandler(BaseHTTPRequestHandler):
server_version = "Femtoshare/{}".format(__version__)
def do_GET(self):
if self.path == "/":
self.send_directory_listing()
return
# check requested filename
filename = urllib.parse.unquote(self.path[1:])
if not self.path.startswith("/") or not self.is_valid_filename(filename):
self.send_error(400, "Invalid file request")
return
self.send_file(filename)
def do_POST(self):
# check uploaded file (note that files within cgi.FieldStorage don't need to be explicitly closed)
try:
form = cgi.FieldStorage(fp=self.rfile, headers=self.headers, environ={"REQUEST_METHOD": "POST", "CONTENT_TYPE": self.headers["Content-Type"]})
except:
self.send_error(400, "Invalid file upload parameters")
return
if "delete_name" in form: # file delete
filename = form["delete_name"].value
if not self.is_valid_filename(filename):
self.send_error(400, "Invalid filename for file deletion")
return
# delete uploaded file from disk
local_path = os.path.join(FILES_DIRECTORY, filename)
os.remove(local_path)
elif "upload_file" in form: # file upload
filename = form["upload_file"].filename
if not self.is_valid_filename(filename):
self.send_error(400, "Invalid filename for file upload")
return
# store uploaded file to disk
local_path = os.path.join(FILES_DIRECTORY, filename)
with open(local_path, "wb") as f:
copyfileobj(form["upload_file"].file, f)
else:
self.send_error(400, "Invalid file upload parameters")
return
self.send_directory_listing()
def send_file(self, file_path, headers_only=False):
try:
f = open(os.path.join(FILES_DIRECTORY,file_path), "rb")
except OSError:
self.send_error(404, "File not found")
return
try:
file_info = os.stat(f.fileno())
self.send_response(200)
self.send_header("Content-type", "application/octet-stream")
self.send_header("Content-Length", str(file_info.st_size))
self.send_header("Last-Modified", self.date_time_string(file_info.st_mtime))
self.end_headers()
copyfileobj(f, self.wfile)
finally:
f.close()
def send_directory_listing(self):
table_entries = []
try:
for dir_entry in os.scandir(FILES_DIRECTORY):
if not dir_entry.is_file(follow_symlinks=False): continue
file_info = dir_entry.stat()
table_entries.append((dir_entry.name, file_info.st_size, datetime.fromtimestamp(file_info.st_mtime)))
except OSError:
self.send_error(500, "Error listing directory contents")
return
response = (
"<!doctype html>\n" +
"<html>\n" +
" <head>\n" +
" <meta charset=\"utf-8\">\n" +
" <title>{0}</title>\n".format(title) +
" <style type=\"text/css\">\n" +
" body { font-family: monospace; width: 80%; margin: 5em auto; text-align: center; }\n" +
" h1 { font-size: 4em; margin: 0; }\n" +
" a { color: inherit; }\n" +
" table { border-collapse: collapse; width: 100%; }\n" +
" table th, table td { border-top: 1px solid #f0f0f0; text-align: left; padding: 0.2em 0.5em; }\n" +
" table th { border-top: none; }\n" +
" body > form { margin: 1em auto; padding: 1em; display: inline-block; border-top: 0.2em solid black; }\n" +
" body > form input { font-family: inherit; margin: 0 1em; }\n" +
" table input { font-family: inherit; margin: 0; font-size: 0.8em; }\n" +
" p { font-weight: bold; }\n" +
" </style>\n" +
" </head>\n" +
" <body>\n" +
" <h1>{0}</h1>\n".format(title) +
" <form enctype=\"multipart/form-data\" method=\"post\"><input name=\"upload_file\" type=\"file\" /><input type=\"submit\" value=\"upload\" /></form>\n" +
(
" <table>\n" +
" <thead><tr><th>name</th><th>size</th><th>last modified</th><th>actions</th></tr></thead>\n" +
" <tbody>\n" +
"".join(
" <tr><td><a href=\"{url}\">{name}</a></td><td>{size:,}</td><td>{last_modified}</td><td><form method=\"post\"><input name=\"delete_name\" type=\"hidden\" value=\"{name}\" /><input type=\"submit\" value=\"delete\" /></form></td></tr>\n".format(
url=urllib.parse.quote(name), name=html.escape(name), size=size, last_modified=last_modified.isoformat()
)
for name, size, last_modified in table_entries
) +
" </tbody>\n" +
" </table>\n"
if table_entries else
" <p>(uploaded files will be visible here)</p>\n"
) +
" </body>\n" +
"</html>\n"
)
response_bytes = response.encode("utf8")
self.send_response(200)
self.send_header("Content-Type", "text/html")
self.send_header("Content-Length", str(len(response_bytes)))
self.end_headers()
self.wfile.write(response_bytes)
def is_valid_filename(self, filename):
if not os.path.realpath(os.path.join(FILES_DIRECTORY,filename)).startswith(os.path.abspath(FILES_DIRECTORY)):
return False
if (os.path.sep is not None and os.path.sep in filename) or (os.path.altsep is not None and os.path.altsep in filename): # check for filesystem separators
return False
if filename in (os.pardir, os.curdir): # check for reserved filenames
return False
if re.match(r"^[\w `!@\$\^\(\)\-=_\[\];',\.]*$", filename) is None: # check for invalid characters
return False
return True
if __name__ == '__main__':
os.makedirs(FILES_DIRECTORY, exist_ok=True) # ensure files directory exists
server = HTTPServer(SERVER_ADDRESS, FemtoshareRequestHandler)
try:
server.serve_forever()
except KeyboardInterrupt:
pass