-
Notifications
You must be signed in to change notification settings - Fork 1
/
web_server.py
359 lines (292 loc) · 12 KB
/
web_server.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
# HTTP server based on the uasyncio example script by J.G. Wezensky
import gc;
import uasyncio as asyncio;
from utils import exists;
#webroot = 'wwwroot';
webroot = '';
default = 'index.py';
SIZE_BUFFER = const( 512);
SIZE_MAX_HTTP_HEADER = const( 512); # TODO
SIZE_MAX_HTTP_TOTAL = const(1024);
# Looks up the content-type based on the file extension
def get_mime_type(file): #{
print("get_mime_type(" + str(file) + ")");
if file.endswith(".html"): return "text/html" , False;
if file.endswith(".css" ): return "text/css" , True;
if file.endswith(".js" ): return "text/javascript", True;
if file.endswith(".png" ): return "image/png" , True;
if file.endswith(".gif" ): return "image/gif" , True;
if file.endswith(".jpeg") or file.endswith(".jpg"):
return "image/jpeg" , True;
return "text/plain", False;
#}
# Breaks an HTTP uri request into its parts and boils it down to a physical file
# (if possible)
def decode_path(uri): #{
global webroot, default;
if (len(uri) == 0): return;
print("decode_path(" + str(uri) + ")");
try: #{
path, varstr = uri.split('?', 1);
vararray = varstr.split('&');
varstr = None;
except: #}{
path = uri;
vararray = [];
#}
print("uri:", uri, "\npath:", path, "\nvararray:", vararray);
# uri: '/web_config_wifi.longname.html?param1=stuff1¶m2=stuff2'
# path: '/web_config_wifi.longname.html'
# vararray: ['param1=stuff1', 'param2=stuff2']
# dict-ify the vararray pair array
# (exception for this will be caught in caller)
vardict = dict(map(lambda s : map(str.strip, s.split('=', 1)), vararray));
print("vardict:", vardict);
# vardict: {'param1': 'stuff1', 'param2': 'stuff2'}
# Check for use of default document
if path == '/': #{
path = default;
else: #}{
path = path[1:];
#}
# Return the physical path of the response file
return webroot + '/' + path, vardict;
#}
# Larger requests would not all be delivered so we need to do multiple calls to
# piece it all together
async def read_complete_req(reader, writer): #{
print("\nread_complete_req()");
req = {};
headers = {};
body = '';
clen = 0;
# Read in complete request
while True: #{
more = '';
# TODO: Add timer here to trigger timeout?
more = (await reader.read(SIZE_BUFFER));
more = more.decode() if more is not None else '';
if (len(more) < 1): #{
print("[ERROR ] Failed to retrieve any more data");
#break;
print("RETURNING: ", False, ", ", req, ", ", headers, ", ", body);
return False, req, headers, body;
#}
if (len(body) + len(more) > SIZE_MAX_HTTP_TOTAL): #{
print("[ERROR ] HTTP payload beyond max: "
+ str(len(body) + len(more))
+ " > " + str(SIZE_MAX_HTTP_TOTAL)
);
writer.write("HTTP/1.0 413 Payload Too Large\r\n\r\n");
await writer.wait_closed();
print("RETURNING: ", False, ", ", req, ", ", headers, ", ", body);
return False, req, headers, body;
#}
body = body + more;
# Do we still need to get our headers?
if (len(req) == 0): #{
# Haven't split anything yet
# Have we at least got the headers?
try: #{
firstbit, body = body.split('\r\n\r\n', 1);
# Yep
try: #{
req, firstbit = firstbit.split('\r\n', 1);
except ValueError: #}{
req = firstbit;
firstbit = '';
#}
headers = firstbit.split('\r\n');
firstbit = None;
# req = [str] request (ie. "GET /puppy.png HTTP/1.1")
# headers = [arr] array of [str] header items
# body = [str] body
keys = ['method', 'uri', 'protocol'];
req = req.split();
if (not len(req) == 3): #{
print("[ERROR ] HTTP Bad Request: Request not 3 elements: ", req);
await writer.awrite("HTTP/1.0 400 Bad Request\r\n\r\n");
#await writer.aclose();
return False, {}, {}, body;
#}
req = dict(zip(keys, req));
# req = [dict] of [str] request parts
# (ie. ["method" = "GET", "uri" = "/puppy.png", "protocol" = "HTTP/1.1"])
print("REQUEST: ", req);
# dict-ify the header pair array
try: #{
headers = dict(map(lambda s : map(str.strip, s.split(':', 1)), headers));
except: #}{
print("[ERROR ] HTTP Bad Request: Failed to dict-ifying headers: ", headers);
await writer.awrite("HTTP/1.0 400 Bad Request\r\n\r\n");
#await writer.aclose();
return False, {}, {}, body;
#}
# Lowercase the keys
headers = dict((k.lower(), v) for k,v in headers.items());
print("HEADERS: ", headers);
try: #{
clen = int(headers['content-length']);
except: #}{
if not req['method'] == "POST": #{
# Doesn't (seem to) contain Content-Length so assume
# we're done and return?
break;
#}
print("[ERROR ] HTTP Content-Length Required");
await writer.awrite("HTTP/1.0 411 Length Required\r\n\r\n");
#await writer.aclose();
return False, req, headers, body;
#}
except ValueError: #}{
# No headers yet...
pass;
#}
#}
# Have we got Content-Length yet?
if clen > 0: #{
# And if we do, have we got MORE data than that?
if len(body) > clen: #{
print("[WARN ] HTTP Content > Content-Length: "
+ str(len(body))
+ " > " + str(clen)
);
# And if we do, have we just the RIGHT amount of data?
elif len(body) == clen: #}{
# If we do, let's stop this loopy business
break;
#}
# Continue until we read clen bytes of body
#}
# Continue until we get our headers and determine length of body
#}
#print("clen: " + str(clen));
#print("req: " + req);
#print("headers: " + str(headers));
#print("body: " + body);
print("RETURNING: ", True, ", ", req, ", ", headers, ", ", body, "\n");
return True, req, headers, body;
#}
async def serve(reader, writer): #{
print("serve()");
didread, req, headers, body = await read_complete_req(reader, writer);
# [bool] didread - Did it work
# [dict] req - The request:
# [str] method - The request method
# [str] protocol - The protocol (eg. "HTTP/1.1")
# [str] uri - The URI (eg. "/web_config.html")
# [dict] headers - The headers for the request:
# [str] user-agent - The user agent
# [str] accept - What mimetypes are accepted
# etc
# [str] body - The body of the request
if (not didread): #{
# Failed to retrieve
await writer.aclose();
return;
#}
didread = None;
headers = None; # Don't care about headers right now
gc.collect();
try: #{
file, vardict = decode_path(req['uri']);
except: #}{
print("[ERROR ] HTTP Bad Request: Failed to dict-ifying vararray for URI: ", req['uri']);
await writer.awrite("HTTP/1.0 400 Bad Request\r\n\r\n");
await writer.aclose();
return;
#}
gc.collect();
print("[" + req['method'] + "] Serving file: " + file + "(" + str(vardict) + ")");
if exists(file): #{
mime_type, cacheable = get_mime_type(file);
# If the name is a python file, treat it as a module that we import and
# then call a function named after the method of the request. We also
# call a function that's the name of the method, prefixed with "end", if
# it exists (ie. GET() and endGET()).
#
# NOTE: You MUST output the HTTP response header (eg. "HTTP/1.0 200
# OK"), it's trailing new line, the Content-Type header and it's
# subsequent blank line) yourself!
if file.endswith(".py"): #{
mod = None;
print("EXECUTE:", file);
if (not vardict): vardict = {};
try: #{
modpath = file.rsplit(".", 1)[0];
modname = modpath.rsplit("/", 1)[1];
mod = __import__(modpath);
except: #}{
print("[ERROR ] Failed to import module: %s" % modpath);
await writer.awrite("HTTP/1.0 500 Internal Server Error: Exec1\r\n\r\n");
await writer.aclose();
gc.collect();
return;
#}
for func in [modname, req['method'], 'end' + req['method']]: #{
print("Looking for func: %s" % func);
try: #{
modfunc = getattr(mod, func);
except: #}{
if (not func == req['method']): continue;
# req['method'] is the only required function
print("[ERROR ] Failed to prepare function: %s.%s" % (modname, func));
await writer.awrite("HTTP/1.0 500 Internal Server Error: Exec2\r\n\r\n");
await writer.aclose();
gc.collect();
return;
#}
print("Rendering func: %s" % func);
page = 1;
next_offset = 0;
while (next_offset >= 0): #{
buffer, next_offset = modfunc(SIZE_BUFFER, next_offset, vardict, body);
print("---\nBUFFER[page:%d, next_offset:%d]:\n" % (page, next_offset));
print(buffer, "\n---\n");
####await writer.awrite("HTTP/1.0 200 OK\r\n");
##### FIXME: if buffer comes back as nothing (such as if I
##### async.sleep in the func) then write.awrite fails as buffer
##### is of type 'generator' and doesn't have .len()
###await writer.awrite(buffer);
##if (buffer): await writer.awrite(buffer);
#if (buffer is not None): await writer.awrite(buffer);
if (len(buffer) >= 1): #{
await writer.awrite(buffer.replace('\n', '\r\n'));
page = page + 1;
#}
#}
#}
else: #}{
# Headers
await writer.awrite("HTTP/1.0 200 OK\r\n");
await writer.awrite("Content-Type: {}\r\n".format(mime_type));
if cacheable: #{
await writer.awrite("Cache-Control: max-age=86400\r\n");
#}
await writer.awrite("\r\n");
f = open(file, "rb");
buffer = f.read(SIZE_BUFFER);
while buffer != b'': #{
await writer.awrite(buffer);
buffer = f.read(SIZE_BUFFER);
#}
f.close();
gc.collect();
#}
else: #}{
print("[ERROR ] Not Found");
await writer.awrite("HTTP/1.0 404 Not Found\r\n\r\n");
#}
await writer.aclose();
gc.collect();
#}
def start(): #{
print("Initialising event loop...");
loop = asyncio.get_event_loop();
print("Starting server...");
loop.create_task(asyncio.start_server(serve, "0.0.0.0", 80, 20));
print("Entering loop...");
loop.run_forever();
loop.close();
#}
# vim:ts=4:tw=80:sw=4:et:ai:si