|
|
- #!/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
|