Skip to content

Commit 2c459db

Browse files
committed
Initial commit
1 parent 73b89d5 commit 2c459db

File tree

2 files changed

+117
-6
lines changed

2 files changed

+117
-6
lines changed

README.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,23 @@
1-
# basic-python-http-server
1+
# Basic Python HTTP Server (BPHS)
2+
3+
Command-line static HTTP server build upon Python [`HTTPServer`][1] from [standard library][2].
4+
5+
No additional dependencies required.
6+
7+
## Installation
8+
9+
One-file bundled executable can be downloaded from the **Releases** section.
10+
11+
## Usage
12+
13+
```txt
14+
bphs [-h] [-p] [-d] [-l]
15+
16+
-h, --help show help message and exit
17+
-p, --port port to use [8080]
18+
-d, --dir directory to serve [current directory]
19+
-l, --listing enable directory listing
20+
```
21+
22+
[1]: https://github.com/python/cpython/blob/3.12/Lib/http/server.py
23+
[2]: https://docs.python.org/3/library/http.server.html

src/main.py

Lines changed: 94 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,99 @@
1-
""" Test """
1+
""" Basic Python HTTP Server """
22

3+
import os
4+
import logging
5+
import argparse
6+
import itertools
7+
from io import BytesIO
8+
from pathlib import Path
9+
from functools import partial
10+
from socketserver import ThreadingMixIn
11+
from http.server import HTTPServer, SimpleHTTPRequestHandler
312

4-
def main() -> None:
5-
""" Main """
6-
print("Hello")
13+
14+
logging.basicConfig(level=logging.INFO,
15+
format='%(asctime)s | %(levelname)s | %(message)s')
16+
17+
18+
class CustomHelpFormatter(argparse.HelpFormatter):
19+
""" Custom Help Formatter to fix additional spaces that appear if metavar is empty """
20+
21+
def _format_action_invocation(self, action: argparse.Action) -> str:
22+
default_format = super()._format_action_invocation(action)
23+
return default_format.replace(" ,", ",")
24+
25+
26+
class ThreadingBasicServer(ThreadingMixIn, HTTPServer):
27+
""" Enable threading for HTTP Server """
28+
29+
30+
class BasicHTTPRequestHandler(SimpleHTTPRequestHandler):
31+
""" Custom Request Handler """
32+
33+
def __init__(self, *handler_args, **handler_kwargs) -> None:
34+
self.dir_listing = handler_kwargs.pop('dir_listing', False)
35+
super().__init__(*handler_args, **handler_kwargs)
36+
self.follow_symlinks = False
37+
38+
# https://en.wikipedia.org/wiki/List_of_Unicode_characters#Control_codes
39+
_control_char_table = str.maketrans({c: fr'\x{c:02x}' for c in
40+
itertools.chain(range(0x20), range(0x7f, 0xa0))})
41+
_control_char_table[ord('\\')] = r'\\'
42+
43+
def log_message(self, *log_args) -> None:
44+
""" Custom log message formatter """
45+
message: str = log_args[0] % log_args[1:]
46+
logging.info("%s - - %s",
47+
self.address_string(),
48+
message.translate(self._control_char_table))
49+
50+
def list_directory(self, path: str | os.PathLike[str]) -> BytesIO | None:
51+
""" Add control over directory listing """
52+
if not self.dir_listing:
53+
self.send_error(403, "Directory listing is disabled")
54+
return None
55+
return super().list_directory(path)
56+
57+
58+
def basic_http_server(port: int, public_dir: Path, dir_listing: bool) -> None:
59+
""" Starts a basic HTTP server """
60+
if not public_dir.exists() or not public_dir.is_dir():
61+
logging.error("Directory \"%s\" doesn't exist", public_dir)
62+
return
63+
64+
logging.info("Initializing Basic HTTP Server")
65+
try:
66+
httpd = ThreadingBasicServer(("", port), partial(
67+
BasicHTTPRequestHandler, directory=public_dir, dir_listing=dir_listing))
68+
69+
logging.info("Available on port %s", port)
70+
httpd.serve_forever()
71+
except PermissionError as error:
72+
logging.error("%s. Port is already in use?", error)
73+
74+
75+
def parse_arguments() -> argparse.Namespace:
76+
""" Parses command-line arguments """
77+
parser = argparse.ArgumentParser(
78+
prog="bphs", description="Basic Python HTTP Server",
79+
formatter_class=CustomHelpFormatter
80+
)
81+
82+
parser.add_argument("-p", "--port", metavar="",
83+
default=8080, type=int,
84+
help="port to use [8080]")
85+
parser.add_argument("-d", "--dir", metavar="",
86+
default=Path(os.getcwd()), type=Path,
87+
help="directory to serve [current directory]")
88+
parser.add_argument("-l", "--listing", action="store_true",
89+
help="enable directory listing")
90+
return parser.parse_args()
791

892

993
if __name__ == "__main__":
10-
main()
94+
args = parse_arguments()
95+
96+
try:
97+
basic_http_server(args.port, args.dir, args.listing)
98+
except KeyboardInterrupt:
99+
logging.info("Basic HTTP Server stopped")

0 commit comments

Comments
 (0)