Skip to content

Instantly share code, notes, and snippets.

@haxwithaxe
Created June 22, 2026 21:51
Show Gist options
  • Select an option

  • Save haxwithaxe/7de631d474500a3b6ca0393a3a65b350 to your computer and use it in GitHub Desktop.

Select an option

Save haxwithaxe/7de631d474500a3b6ca0393a3a65b350 to your computer and use it in GitHub Desktop.
A script to quickly share a file over http without exposing an entire directory (on-demand snakeoil TLS in the future)
#!/usr/bin/env python3
"""Quickly share a file or files without exposing a whole directory."""
import argparse
import contextlib
import http
import http.server
import html
import io
import logging
import os
import pathlib
import select
import socket
import sys
import urllib
from typing import Any, ClassVar, Self
from xml import etree
from xml.etree.ElementTree import Element, SubElement
__author__ = 'Chris (haxwithaxe) Koepke'
__copyright__ = 'Copyright 2026, Chris Koepke'
__license__ = 'GPL-3.0-only'
DEFAULT_STYLE: str = '''
@media (prefers-color-scheme: dark) {
body {
background: black;
color: white;
}
'''
SOCKET_EXCEPTIONS: list[type[Exception]] = [
ConnectionResetError,
BrokenPipeError,
TimeoutError,
OSError,
]
logging.basicConfig()
log: logging.Logger = logging.getLogger()
def _wrap_code(
code: bytes | bytearray,
style: dict[str, str] | None = None,
encoding: str = 'utf-8',
) -> bytearray:
pre: Element = Element('pre')
pre.text = code.decode(encoding)
log.debug(
'Wrapping pre element: %s',
etree.ElementTree.tostring(
pre,
method='html',
),
)
return bytearray(
_wrap_html_content(
pre,
title='Code Formated',
encoding=encoding,
),
)
def _directory_list_item(
text: str, href: str, is_dir: bool = False, is_symlink: bool = False
) -> Element:
additional_classes: list[str] = []
href: str = urllib.parse.quote(href, errors='surrogatepass')
text: str = html.escape(text, quote=False)
if is_dir:
additional_classes.append('directory-link')
text = f'{text}/'
href = f'{href}/'
if is_symlink:
additional_classes.append('symlink-link')
text = f'{text}@'
li: Element = Element(
'li',
attrib={'class': ' '.join(['directory-listing', *additional_classes])},
)
anchor: Element = SubElement(
li,
'a',
attrib={
'href': href,
'class': ' '.join(['directory-listing-link', *additional_classes]),
},
)
anchor.text = text
return li
def _wrap_html_content(
*content: Element,
title: str = '',
style: dict[str, str] | None = None,
encoding: str = 'utf-8',
language: str = 'en',
head_content: list[Element | dict[str, dict | str] | str] | None = None,
) -> bytes:
log.debug('Got content to wrap: %s', content)
script_name: str = pathlib.Path(__file__).stem
doc: Element = Element('html', attrib={'lang': language})
head: Element = SubElement(doc, 'head')
head.append(Element('meta', attrib={'charset': encoding}))
head.append(
Element(
'meta',
attrib={
'name': 'viewport',
'content': 'width=device-width, initial-scale=1',
},
)
)
head_title: Element = SubElement(head, 'title')
head_title.text = f'{script_name} {title}'
page_style: Element = SubElement(head, 'style')
page_style.text = DEFAULT_STYLE
for head_item in head_content or []:
if isinstance(head_item, str):
head.append(Element(tag=head_item))
elif isinstance(head_item, dict):
head.append(Element(**head_item)) # type: ignore
elif isinstance(head_item, Element):
head.append(head_item)
else:
raise TypeError(
'Got head_content item of unexpected type: '
f'{type(head_item)}: {head_item}', # nofmt
)
body: Element = SubElement(doc, 'body', attrib={'class': 'document-body'})
container: Element = SubElement(body, 'div', attrib={'class': 'content'})
for elem in content or []:
container.append(elem)
html_bytes: bytes = etree.ElementTree.tostring(doc, method='html')
return f'<!DOCTYPE HTML>\n{html_bytes.decode(encoding)}'.encode(
encoding,
'surrogateescape',
)
class DualStackServer(http.server.ThreadingHTTPServer):
"""An HTTP server that picks from IPv4 and IPv6."""
def server_bind(self) -> None:
"""Override of `ThreadingHTTPServer.server_bind`."""
# suppress exception when protocol is IPv4
with contextlib.suppress(*SOCKET_EXCEPTIONS):
self.socket.setsockopt(
socket.IPPROTO_IPV6,
socket.IPV6_V6ONLY,
0,
)
return super().server_bind()
class VirtualHandler(http.server.SimpleHTTPRequestHandler):
"""HTTP Request handler that serves static content from memory."""
content_type: ClassVar[str] = 'application/octet-stream'
content: ClassVar[bytes] = b'<empty content>'
def do_GET(self) -> None:
"""Serve a GET request."""
self.send_response(http.HTTPStatus.OK)
self.send_header('Content-type', self.content_type)
self.send_header('Content-Length', str(len(self.content)))
log.critical('Sending headers: %s', self.headers)
self.end_headers()
log.debug('Sending content: %s', self.content)
self.wfile.write(self.content)
@classmethod
def set_content(cls, content: bytes, content_type: str) -> type[Self]:
"""Prepare the handler with content and content type."""
cls.content = content
cls.content_type = content_type
return cls
class DirectoryHandler(http.server.SimpleHTTPRequestHandler):
"""An HTTP request handler that only serves directory listings."""
target: ClassVar[str] = '.'
def __init__(self, *args, **kwargs) -> None:
if 'directory' in kwargs:
kwargs.pop('directory')
super().__init__(*args, directory=self.target, **kwargs)
def list_directory(self, path) -> io.BytesIO | None:
"""Send an HTML directory listing.
A 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:
directory_list = os.listdir(path)
except OSError:
self.send_error(
http.HTTPStatus.NOT_FOUND,
"No permission to list directory",
)
return
directory_list.sort(key=lambda a: a.lower())
displaypath: str = self.path
displaypath = displaypath.split('#', 1)[0]
displaypath = displaypath.split('?', 1)[0]
try:
displaypath = urllib.parse.unquote(
displaypath,
errors='surrogatepass',
)
except UnicodeDecodeError:
displaypath = urllib.parse.unquote(displaypath)
displaypath = html.escape(displaypath, quote=False)
encoding: str = sys.getfilesystemencoding()
encoded: bytes = self._get_directory_listing_html(
path, directory_list, displaypath, encoding
)
f = io.BytesIO()
f.write(encoded)
f.seek(0)
self.send_response(http.HTTPStatus.OK)
self.send_header("Content-type", "text/html; charset=%s" % encoding)
self.send_header("Content-Length", str(len(encoded)))
self.end_headers()
return f
def _get_directory_listing_html(
self,
path: str,
directory_list: list[str],
displaypath: str,
encoding: str,
) -> bytes:
"""Return an HTML directory listing.
Arguments:
path: The original path given to `list_directory`.
directory_list: The output of `os.listdir`.
displaypath: The human facing directory path.
encoding: The output encoding.
"""
title: str = f'Directory listing for {displaypath}'
heading: Element = Element('h1', attrib={'id': 'title'})
heading.text = title
open_rule: Element = Element('hr', attrib={'id': 'opening-rule'})
listings: Element = Element(
'ul',
attrib={
'class': 'directory-listing-list',
'id': 'directory-listings',
},
)
for name in directory_list:
fullname = os.path.join(path, name)
displayname = linkname = name
listings.append(
_directory_list_item(
displayname,
linkname,
os.path.isdir(fullname),
os.path.islink(fullname),
),
)
close_rule: Element = Element('hr', attrib={'id': 'closing-rule'})
return _wrap_html_content(
heading,
open_rule,
listings,
close_rule,
title=title,
encoding=encoding,
)
@classmethod
def set_dir(cls, directory: pathlib.Path) -> type[Self]:
"""Set the target directory for a request handler."""
cls.target = str(directory.absolute())
return cls
def main() -> None: # noqa: D103
log.setLevel(logging.DEBUG)
parser = argparse.ArgumentParser()
parser.add_argument(
'-b',
'--bind',
metavar='ADDRESS',
help='bind to this address (default: all interfaces)',
)
parser.add_argument(
'-t',
'--target',
default=os.getcwd(),
help=(
'Serve this file or directory (default: current directory). '
'If "-" is given stdin is used.' # nofmt
),
)
parser.add_argument(
'-p',
'--port',
default=0,
type=int,
help='bind to this port (default: %(default)s)',
)
parser.add_argument(
'-m',
'--content-type',
'--mimetype',
metavar='MIMETYPE',
default='application/octet-stream',
help=(
'Mimetype, "text", "code", "html", "binary" (aka '
'"application/octet-stream" default).'
),
)
log.critical('critical')
log.critical('debug')
args: argparse.Namespace = parser.parse_args()
if not args.target:
print(
'The value of -t|--target must be a path to an existing file or '
'directory or "-".', # nofmt
file=sys.stderr,
)
sys.exit(1)
# Prep content-type
log.critical('content-type given as %s', args.content_type)
if args.content_type == 'binary':
content_type = 'application/octet-stream'
elif args.content_type == 'text':
content_type = 'text/plain'
elif args.content_type in ('code', 'html'):
content_type = 'text/html'
else:
content_type = args.content_type
# Handle target, set request handler accordingly
if args.target == '-':
if not select.select(
[
sys.stdin,
],
[],
[],
0.0,
)[0]:
log.critical('No data in stdin')
sys.exit(1)
content: bytearray = bytearray()
while byte := sys.stdin.buffer.read(1):
content.extend(byte)
if len(content.strip()) == 0:
log.warning(
'WARNING: Got only a whitespaces on stdin.'
) # zuban: ignore[unreachable]
else:
log.info('Got %s bytes on stdin.', len(content))
if args.content_type == 'code':
content = _wrap_code(content, encoding='utf-8')
handler: type[http.server.SimpleHTTPRequestHandler] = (
VirtualHandler.set_content(content, content_type)
)
else:
target = pathlib.Path(args.target)
if not target.exists():
print(
f'`{target.absolute()}` does not exist. A file or directory '
'must exist to be shared.', # nofmt
file=sys.stderr,
)
sys.exit(1)
if target.is_dir():
handler: type[http.server.SimpleHTTPRequestHandler] = (
DirectoryHandler.set_dir(target.absolute())
)
else:
if args.content_type == 'code':
content: bytearray = _wrap_code(
target.read_bytes(),
encoding='utf-8',
)
else:
content: bytearray = bytearray(target.read_bytes())
handler: type[http.server.SimpleHTTPRequestHandler] = (
VirtualHandler.set_content(content, content_type)
)
addrinfo: list[Any] = socket.getaddrinfo(
args.bind,
args.port,
type=socket.SOCK_STREAM,
flags=socket.AI_PASSIVE,
)
DualStackServer.address_family = addrinfo[0][0]
sockaddr: tuple[str, int] | Any = addrinfo[0][-1]
with DualStackServer(sockaddr, handler) as httpd:
host, port = httpd.socket.getsockname()[:2]
if host in ('0.0.0.0', '::'):
host = 'all interfaces'
url_host: str = socket.getfqdn()
elif host.startswith('127.') or host == '::1':
host = 'localhost'
url_host: str = 'localhost'
else:
url_host: str = f'[{host}]' if ':' in host else host
print(f'Serving HTTP on port {port} on {host}')
if os.environ.get('NO_COLOR') != '1':
print('\033[38;5;46m\033[48;5;16m', end='')
print(f'http://{url_host}:{port}/', end='')
if os.environ.get('NO_COLOR') != '1':
print('\033[0m', end='')
print() # Add a newline with or without color
try:
httpd.serve_forever()
except KeyboardInterrupt:
print('\nKeyboard interrupt received, exiting.')
sys.exit(0)
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment