Skip to content

Commit f9f8a98

Browse files
authored
Merge pull request #383 from realpython/python-http-server
Materials for Python HTTP Server
2 parents f05647e + a798031 commit f9f8a98

File tree

8 files changed

+208
-0
lines changed

8 files changed

+208
-0
lines changed

python-http-server/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# How to Launch an HTTP Server in One Line of Python Code
2+
3+
This folder provides the code examples for the Real Python tutorial [How to Launch an HTTP Server in One Line of Python Code](https://realpython.com/python-http-server/).
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#!/usr/bin/env python3
2+
3+
print(
4+
"""\
5+
Content-Type: text/html
6+
7+
<!DOCTYPE html>
8+
<html lang="en">
9+
<body>
10+
<h1>Hello, World!</h1>
11+
</body>
12+
</html>"""
13+
)
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#!/usr/bin/env python3
2+
3+
import os
4+
from http.cookies import SimpleCookie
5+
from random import choices
6+
7+
cookies = SimpleCookie(os.getenv("HTTP_COOKIE"))
8+
9+
try:
10+
visits = int(cookies["visits"].value) + 1
11+
except KeyError:
12+
visits = 1
13+
14+
cookies["visits"] = str(visits)
15+
cookies["visits"]["max-age"] = 5 # Expire after 5 seconds
16+
17+
print(
18+
f"""\
19+
Content-Type: text/html
20+
{cookies}
21+
22+
<!DOCTYPE html>
23+
<html lang="en">
24+
<head>
25+
<meta charset="utf-8">
26+
<meta name="viewport" content="width=device-width, initial-scale=1">
27+
<title>Hello from a CGI Script</title>
28+
<style>
29+
body {{
30+
background-color: #{"".join(choices("0123456789abcdef", k=6))};
31+
}}
32+
</style>
33+
</head>
34+
<body>
35+
<h1>CGI Script: {os.getenv("SCRIPT_NAME")}</h1>
36+
<p>You visited this page <b>{cookies["visits"].value}</b> times.</p>
37+
<h2>Environment Variables</h2>
38+
<ul>
39+
<li><b>CONTENT_TYPE:</b> {os.getenv("CONTENT_TYPE")}</li>
40+
<li><b>HTTP_USER_AGENT:</b> {os.getenv("HTTP_USER_AGENT")}</li>
41+
<li><b>QUERY_STRING:</b> {os.getenv("QUERY_STRING")}</li>
42+
<li><b>REQUEST_METHOD:</b> {os.getenv("REQUEST_METHOD")}</li>
43+
</ul>
44+
</body>
45+
</html>"""
46+
)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import argparse
2+
import webbrowser
3+
from http.server import HTTPServer, SimpleHTTPRequestHandler
4+
from ssl import PROTOCOL_TLS_SERVER, SSLContext
5+
6+
from self_signed import SelfSignedCertificate
7+
8+
9+
def main(args):
10+
ssl_context = SSLContext(PROTOCOL_TLS_SERVER)
11+
ssl_context.load_cert_chain(SelfSignedCertificate(args.host).path)
12+
server = HTTPServer((args.host, args.port), SimpleHTTPRequestHandler)
13+
server.socket = ssl_context.wrap_socket(server.socket, server_side=True)
14+
webbrowser.open(f"https://{args.host}:{args.port}/")
15+
server.serve_forever()
16+
17+
18+
def parse_args():
19+
parser = argparse.ArgumentParser()
20+
parser.add_argument("--host", type=str, default="0.0.0.0")
21+
parser.add_argument("--port", type=int, default=443)
22+
return parser.parse_args()
23+
24+
25+
if __name__ == "__main__":
26+
main(parse_args())
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
cffi==1.15.1
2+
cryptography==40.0.2
3+
pyOpenSSL==23.1.1
4+
pycparser==2.21
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from http.server import HTTPServer, SimpleHTTPRequestHandler
2+
from ssl import PROTOCOL_TLS_SERVER, SSLContext
3+
4+
ssl_context = SSLContext(PROTOCOL_TLS_SERVER)
5+
ssl_context.load_cert_chain("/path/to/cert.pem", "/path/to/private.key")
6+
server = HTTPServer(("0.0.0.0", 443), SimpleHTTPRequestHandler)
7+
server.socket = ssl_context.wrap_socket(server.socket, server_side=True)
8+
server.serve_forever()
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import tempfile
2+
from dataclasses import dataclass
3+
from pathlib import Path
4+
5+
from OpenSSL.crypto import (
6+
FILETYPE_PEM,
7+
TYPE_RSA,
8+
X509,
9+
PKey,
10+
dump_certificate,
11+
dump_privatekey,
12+
)
13+
14+
15+
@dataclass(frozen=True)
16+
class SelfSignedCertificate:
17+
host: str = "0.0.0.0"
18+
bits: int = 2048
19+
country: str = "CA"
20+
state: str = "British Columbia"
21+
locality: str = "Vancouver"
22+
organization: str = "Real Python"
23+
organizational_unit: str = "Development"
24+
serial_number: int = 1
25+
expires_on: int = 365 * 24 * 60 * 60
26+
27+
@property
28+
def path(self) -> Path:
29+
key_pair = PKey()
30+
key_pair.generate_key(TYPE_RSA, self.bits)
31+
32+
certificate = X509()
33+
34+
subject = certificate.get_subject()
35+
subject.CN = self.host
36+
subject.C = self.country
37+
subject.ST = self.state
38+
subject.L = self.locality
39+
subject.O = self.organization # noqa
40+
subject.OU = self.organizational_unit
41+
42+
certificate.set_serial_number(self.serial_number)
43+
certificate.gmtime_adj_notBefore(0)
44+
certificate.gmtime_adj_notAfter(self.expires_on)
45+
certificate.set_issuer(subject)
46+
certificate.set_pubkey(key_pair)
47+
certificate.sign(key_pair, "sha256")
48+
49+
with tempfile.NamedTemporaryFile(delete=False) as file:
50+
file.write(dump_privatekey(FILETYPE_PEM, key_pair))
51+
file.write(dump_certificate(FILETYPE_PEM, certificate))
52+
53+
return Path(file.name)
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import json
2+
from functools import cached_property
3+
from http.cookies import SimpleCookie
4+
from http.server import BaseHTTPRequestHandler, HTTPServer
5+
from urllib.parse import parse_qsl, urlparse
6+
7+
8+
class WebRequestHandler(BaseHTTPRequestHandler):
9+
@cached_property
10+
def url(self):
11+
return urlparse(self.path)
12+
13+
@cached_property
14+
def query_data(self) -> dict[str, str]:
15+
return dict(parse_qsl(self.url.query))
16+
17+
@cached_property
18+
def post_data(self) -> bytes:
19+
content_length = int(self.headers.get("Content-Length", 0))
20+
return self.rfile.read(content_length)
21+
22+
@cached_property
23+
def form_data(self):
24+
return dict(parse_qsl(self.post_data.decode("utf-8")))
25+
26+
@cached_property
27+
def cookies(self):
28+
return SimpleCookie(self.headers.get("Cookie"))
29+
30+
def do_GET(self):
31+
self.send_response(200)
32+
self.send_header("Content-Type", "application/json")
33+
self.end_headers()
34+
self.wfile.write(self.get_response().encode("utf-8"))
35+
36+
def do_POST(self):
37+
self.do_GET()
38+
39+
def get_response(self):
40+
return json.dumps(
41+
{
42+
"path": self.url.path,
43+
"query_data": self.query_data,
44+
"post_data": self.post_data.decode("utf-8"),
45+
"form_data": self.form_data,
46+
"cookies": {
47+
name: cookie.value for name, cookie in self.cookies.items()
48+
},
49+
}
50+
)
51+
52+
53+
if __name__ == "__main__":
54+
server = HTTPServer(("0.0.0.0", 8000), WebRequestHandler)
55+
server.serve_forever()

0 commit comments

Comments
 (0)