Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1from net.tcp.server import Server as TcpServer
2from net.tcp.server import Client
3from os.path import Path
4from os.temporary import Directory as TemporaryDirectory
5from http.header_parser import parse_request
6from http import HttpError
7from io.buffered_reader import BufferedReader
8from fiber import Fiber
9from http import get
10from http import post
12_HEADERS_END: bytes = b"\r\n\r\n"
14enum Status:
15 """HTTP response status codes.
17 """
19 Continue = 100
20 Ok = 200
21 Found = 302
22 BadRequest = 400
23 Unauthorized = 401
24 NotFound = 404
25 MethodNotAllowed = 405
26 InternalServerError = 500
28_STATUS_STRINGS: {i64: string} = {
29 i64(Status.Continue): "Continue",
30 i64(Status.Ok): "OK",
31 i64(Status.Found): "Found",
32 i64(Status.BadRequest): "Bad Request",
33 i64(Status.Unauthorized): "Unauthorized",
34 i64(Status.NotFound): "Not Found",
35 i64(Status.MethodNotAllowed): "Method Not Allowed",
36 i64(Status.InternalServerError): "Internal Server Error"
37}
39class ResponseError(Error):
40 status: Status
42trait _Path:
44 func match(self, path: string) -> (bool, [string]?):
45 pass
47class _StringPath(_Path):
48 path: string
50 func match(self, path: string) -> (bool, [string]?):
51 return (path == self.path, None)
53class _RegexPath(_Path):
54 _regex: regex
56 func __init__(self, regex: regex):
57 self._regex = regex
59 func match(self, path: string) -> (bool, [string]?):
60 mo = self._regex.match(path)
62 if mo is None:
63 return (False, None)
64 else:
65 return (True, mo.groups())
67trait Route:
68 """A route.
70 """
72 func on_get(self, request: Request) -> Response:
73 """Handle a GET request.
75 """
77 return EmptyResponse(Status.MethodNotAllowed)
79 func on_post(self, request: Request) -> Response:
80 """Handle a POST request.
82 """
84 return EmptyResponse(Status.MethodNotAllowed)
86 func on_delete(self, request: Request) -> Response:
87 """Handle a DELETE request.
89 """
91 return EmptyResponse(Status.MethodNotAllowed)
93class Request:
94 method: string
95 path: string
96 query: string
97 params: {string: string}
98 fragment: string
99 headers: {string: string}
100 matches: [string]?
102trait Response:
103 """A response.
105 """
107 func write(self, client: Client):
108 """Write the response to the client.
110 """
112class StringResponse(Response):
113 status: Status
114 data: string
116 func __init__(self, data: string, status: Status = Status.Ok):
117 self.status = status
118 self.data = data
120 func write(self, client: Client):
121 body = self.data.to_utf8()
122 client.write(f"HTTP/1.1 200 OK\r\n"
123 f"Content-Length: {body.length()}\r\n"
124 f"\r\n".to_utf8())
125 client.write(body)
127class FileResponse(Response):
128 status: Status
129 path: Path
130 begin: i64?
131 end: i64?
133 func __init__(self,
134 path: Path,
135 status: Status = Status.Ok,
136 begin: i64? = None,
137 end: i64? = None):
138 self.status = status
139 self.path = path
140 self.begin = begin
141 self.end = end
143 func write(self, client: Client):
144 body = self.path.read_binary()
146 if self.begin is not None and self.end is not None:
147 body = body[self.begin:self.end]
149 client.write(f"HTTP/1.1 200 OK\r\n"
150 f"Content-Length: {body.length()}\r\n"
151 f"\r\n".to_utf8())
152 client.write(body)
154class EmptyResponse(Response):
155 status: Status
157 func __init__(self, status: Status = Status.Ok):
158 self.status = status
160 func write(self, client: Client):
161 status = i64(self.status)
162 client.write(f"HTTP/1.1 {status} {_STATUS_STRINGS[status]}\r\n\r\n".to_utf8())
164class _Client(Fiber):
165 client: Client
166 server: Server
168 func run(self):
169 try:
170 self._serve()
171 except Error:
172 pass
174 self.client.disconnect()
176 func _serve(self):
177 buffered_reader = BufferedReader(self.client, 1024)
178 header = buffered_reader.read_until(_HEADERS_END)
180 if header is None:
181 return
183 request_header = parse_request(header, header.length())
184 request = Request(request_header.method,
185 request_header.path,
186 request_header.query,
187 request_header.params,
188 request_header.fragment,
189 request_header.headers,
190 None)
192 try:
193 response = self._handle_request(request)
194 except ResponseError as error:
195 response = EmptyResponse(error.status)
196 except Error:
197 response = EmptyResponse(Status.InternalServerError)
199 response.write(self.client)
201 func _handle_request(self, request: Request) -> Response:
202 route, request.matches = self.server._find_route(request.path)
204 match request.method:
205 case "GET":
206 return route.on_get(request)
207 case "POST":
208 return route.on_post(request)
209 case "DELETE":
210 return route.on_delete(request)
211 case _:
212 return EmptyResponse(Status.MethodNotAllowed)
215class Server:
216 """A HTTP server.
218 """
220 _routes: [(_Path, Route)]
222 func __init__(self):
223 self._routes = []
225 func add_route(self, path: string, route: Route):
226 """Add a route for given path.
228 """
230 self._routes.append((_StringPath(path), route))
232 # ToDo: Should be overloaded add_route().
233 func add_route_regex(self, path: regex, route: Route):
234 """Add a route for given path.
236 """
238 self._routes.append((_RegexPath(path), route))
240 func serve(self, port: i64):
241 """Serve HTTP requests.
243 """
245 server = TcpServer()
246 server.listen(port)
248 while True:
249 client = server.accept()
250 client_fiber = _Client(client, self)
251 client_fiber.start()
253 func _find_route(self, request_path: string) -> (Route, [string]?):
254 for path, route in self._routes:
255 is_match, matches = path.match(request_path)
257 if is_match:
258 return route, matches
260 raise ResponseError(Status.NotFound)
262class _GetStringRoute(Route):
264 func on_get(self, request: Request) -> Response:
265 return StringResponse("Hello!")
267class _GetFileRoute(Route):
269 func on_get(self, request: Request) -> Response:
270 return FileResponse(Path(request.path))
272class _BasicServer(Fiber):
273 port: i64
275 func run(self):
276 try:
277 server = Server()
278 server.add_route("/index.html", _GetStringRoute())
279 server.add_route_regex(re"/.*", _GetFileRoute())
280 server.serve(self.port)
281 except:
282 pass
284test basic():
285 port = 50654
286 server = _BasicServer(port)
287 server.start()
288 response = get("localhost", port, "/index.html")
289 assert response.status_code == 200
290 assert response.content == b"Hello!"
292 tmpdir = TemporaryDirectory()
293 a_txt = tmpdir.path().join("a.txt")
294 a_txt.write_text("Foo!")
295 assert get("localhost", port, a_txt.to_string()).content == b"Foo!"
296 b_txt = tmpdir.path().join("b.txt")
297 b_txt.write_text("Bar!")
298 assert get("localhost", port, b_txt.to_string()).content == b"Bar!"
300 try:
301 post("localhost", b"", port)
302 assert False
303 except HttpError as error:
304 assert "405" in error.message
306 response = post("localhost", b"", port, check=False)
307 assert response.status_code == 405
309 server.cancel()
310 server.join()