Hide keyboard shortcuts

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 

11 

12_HEADERS_END: bytes = b"\r\n\r\n" 

13 

14enum Status: 

15 """HTTP response status codes. 

16 

17 """ 

18 

19 Continue = 100 

20 Ok = 200 

21 Found = 302 

22 BadRequest = 400 

23 Unauthorized = 401 

24 NotFound = 404 

25 MethodNotAllowed = 405 

26 InternalServerError = 500 

27 

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} 

38 

39class ResponseError(Error): 

40 status: Status 

41 

42trait _Path: 

43 

44 func match(self, path: string) -> (bool, [string]?): 

45 pass 

46 

47class _StringPath(_Path): 

48 path: string 

49 

50 func match(self, path: string) -> (bool, [string]?): 

51 return (path == self.path, None) 

52 

53class _RegexPath(_Path): 

54 _regex: regex 

55 

56 func __init__(self, regex: regex): 

57 self._regex = regex 

58 

59 func match(self, path: string) -> (bool, [string]?): 

60 mo = self._regex.match(path) 

61 

62 if mo is None: 

63 return (False, None) 

64 else: 

65 return (True, mo.groups()) 

66 

67trait Route: 

68 """A route. 

69 

70 """ 

71 

72 func on_get(self, request: Request) -> Response: 

73 """Handle a GET request. 

74 

75 """ 

76 

77 return EmptyResponse(Status.MethodNotAllowed) 

78 

79 func on_post(self, request: Request) -> Response: 

80 """Handle a POST request. 

81 

82 """ 

83 

84 return EmptyResponse(Status.MethodNotAllowed) 

85 

86 func on_delete(self, request: Request) -> Response: 

87 """Handle a DELETE request. 

88 

89 """ 

90 

91 return EmptyResponse(Status.MethodNotAllowed) 

92 

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]? 

101 

102trait Response: 

103 """A response. 

104 

105 """ 

106 

107 func write(self, client: Client): 

108 """Write the response to the client. 

109 

110 """ 

111 

112class StringResponse(Response): 

113 status: Status 

114 data: string 

115 

116 func __init__(self, data: string, status: Status = Status.Ok): 

117 self.status = status 

118 self.data = data 

119 

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) 

126 

127class FileResponse(Response): 

128 status: Status 

129 path: Path 

130 begin: i64? 

131 end: i64? 

132 

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 

142 

143 func write(self, client: Client): 

144 body = self.path.read_binary() 

145 

146 if self.begin is not None and self.end is not None: 

147 body = body[self.begin:self.end] 

148 

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) 

153 

154class EmptyResponse(Response): 

155 status: Status 

156 

157 func __init__(self, status: Status = Status.Ok): 

158 self.status = status 

159 

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()) 

163 

164class _Client(Fiber): 

165 client: Client 

166 server: Server 

167 

168 func run(self): 

169 try: 

170 self._serve() 

171 except Error: 

172 pass 

173 

174 self.client.disconnect() 

175 

176 func _serve(self): 

177 buffered_reader = BufferedReader(self.client, 1024) 

178 header = buffered_reader.read_until(_HEADERS_END) 

179 

180 if header is None: 

181 return 

182 

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) 

191 

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) 

198 

199 response.write(self.client) 

200 

201 func _handle_request(self, request: Request) -> Response: 

202 route, request.matches = self.server._find_route(request.path) 

203 

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) 

213 

214 

215class Server: 

216 """A HTTP server. 

217 

218 """ 

219 

220 _routes: [(_Path, Route)] 

221 

222 func __init__(self): 

223 self._routes = [] 

224 

225 func add_route(self, path: string, route: Route): 

226 """Add a route for given path. 

227 

228 """ 

229 

230 self._routes.append((_StringPath(path), route)) 

231 

232 # ToDo: Should be overloaded add_route(). 

233 func add_route_regex(self, path: regex, route: Route): 

234 """Add a route for given path. 

235 

236 """ 

237 

238 self._routes.append((_RegexPath(path), route)) 

239 

240 func serve(self, port: i64): 

241 """Serve HTTP requests. 

242 

243 """ 

244 

245 server = TcpServer() 

246 server.listen(port) 

247 

248 while True: 

249 client = server.accept() 

250 client_fiber = _Client(client, self) 

251 client_fiber.start() 

252 

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) 

256 

257 if is_match: 

258 return route, matches 

259 

260 raise ResponseError(Status.NotFound) 

261 

262class _GetStringRoute(Route): 

263 

264 func on_get(self, request: Request) -> Response: 

265 return StringResponse("Hello!") 

266 

267class _GetFileRoute(Route): 

268 

269 func on_get(self, request: Request) -> Response: 

270 return FileResponse(Path(request.path)) 

271 

272class _BasicServer(Fiber): 

273 port: i64 

274 

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 

283 

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!" 

291 

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!" 

299 

300 try: 

301 post("localhost", b"", port) 

302 assert False 

303 except HttpError as error: 

304 assert "405" in error.message 

305 

306 response = post("localhost", b"", port, check=False) 

307 assert response.status_code == 405 

308 

309 server.cancel() 

310 server.join()