Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Ardupilot
GitHub Repository: Ardupilot/ardupilot
Path: blob/master/Tools/AP_Periph/Web/scripts/pppgw_webui.lua
9354 views
1
--[[
2
example script to test lua socket API
3
--]]
4
5
---@diagnostic disable: param-type-mismatch
6
---@diagnostic disable: need-check-nil
7
---@diagnostic disable: redundant-parameter
8
---@diagnostic disable: undefined-field
9
10
PARAM_TABLE_KEY = 47
11
PARAM_TABLE_PREFIX = "WEB_"
12
13
-- add a parameter and bind it to a variable
14
function bind_add_param(name, idx, default_value)
15
assert(param:add_param(PARAM_TABLE_KEY, idx, name, default_value), string.format('could not add param %s', name))
16
return Parameter(PARAM_TABLE_PREFIX .. name)
17
end
18
19
-- Setup Parameters
20
assert(param:add_table(PARAM_TABLE_KEY, PARAM_TABLE_PREFIX, 6), 'net_test: could not add param table')
21
22
--[[
23
// @Param: WEB_ENABLE
24
// @DisplayName: enable web server
25
// @Description: enable web server
26
// @Values: 0:Disabled,1:Enabled
27
// @User: Standard
28
--]]
29
local WEB_ENABLE = bind_add_param('ENABLE', 1, 1)
30
31
--[[
32
// @Param: WEB_BIND_PORT
33
// @DisplayName: web server TCP port
34
// @Description: web server TCP port
35
// @Range: 1 65535
36
// @User: Standard
37
--]]
38
local WEB_BIND_PORT = bind_add_param('BIND_PORT', 2, 80)
39
40
--[[
41
// @Param: WEB_DEBUG
42
// @DisplayName: web server debugging
43
// @Description: web server debugging
44
// @Values: 0:Disabled,1:Enabled
45
// @User: Advanced
46
--]]
47
local WEB_DEBUG = bind_add_param('DEBUG', 3, 0)
48
49
--[[
50
// @Param: WEB_BLOCK_SIZE
51
// @DisplayName: web server block size
52
// @Description: web server block size for download
53
// @Range: 1 65535
54
// @User: Advanced
55
--]]
56
local WEB_BLOCK_SIZE = bind_add_param('BLOCK_SIZE', 4, 10240)
57
58
--[[
59
// @Param: WEB_TIMEOUT
60
// @DisplayName: web server timeout
61
// @Description: timeout for inactive connections
62
// @Units: s
63
// @Range: 0.1 60
64
// @User: Advanced
65
--]]
66
local WEB_TIMEOUT = bind_add_param('TIMEOUT', 5, 2.0)
67
68
--[[
69
// @Param: WEB_SENDFILE_MIN
70
// @DisplayName: web server minimum file size for sendfile
71
// @Description: sendfile is an offloading mechanism for faster file download. If this is non-zero and the file is larger than this size then sendfile will be used for file download
72
// @Range: 0 10000000
73
// @User: Advanced
74
--]]
75
local WEB_SENDFILE_MIN = bind_add_param('SENDFILE_MIN', 6, 100000)
76
77
if WEB_ENABLE:get() ~= 1 then
78
periph:can_printf("WebServer: disabled")
79
return
80
end
81
82
periph:can_printf(string.format("WebServer: starting on port %u", WEB_BIND_PORT:get()))
83
84
local sock_listen = Socket(0)
85
local clients = {}
86
87
local DOCTYPE = "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 3.2 Final//EN\">"
88
local SERVER_VERSION = "net_webserver 1.0"
89
local CONTENT_TEXT_HTML = "text/html;charset=UTF-8"
90
local CONTENT_OCTET_STREAM = "application/octet-stream"
91
92
local HIDDEN_FOLDERS = { "@SYS", "@ROMFS", "@MISSION", "@PARAM" }
93
94
local MNT_PREFIX = "/mnt"
95
local MNT_PREFIX2 = MNT_PREFIX .. "/"
96
97
local MIME_TYPES = {
98
["apj"] = CONTENT_OCTET_STREAM,
99
["dat"] = CONTENT_OCTET_STREAM,
100
["o"] = CONTENT_OCTET_STREAM,
101
["obj"] = CONTENT_OCTET_STREAM,
102
["lua"] = "text/x-lua",
103
["py"] = "text/x-python",
104
["shtml"] = CONTENT_TEXT_HTML,
105
["js"] = "text/javascript",
106
-- thanks to https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
107
["aac"] = "audio/aac",
108
["abw"] = "application/x-abiword",
109
["arc"] = "application/x-freearc",
110
["avif"] = "image/avif",
111
["avi"] = "video/x-msvideo",
112
["azw"] = "application/vnd.amazon.ebook",
113
["bin"] = "application/octet-stream",
114
["bmp"] = "image/bmp",
115
["bz"] = "application/x-bzip",
116
["bz2"] = "application/x-bzip2",
117
["cda"] = "application/x-cdf",
118
["csh"] = "application/x-csh",
119
["css"] = "text/css",
120
["csv"] = "text/csv",
121
["doc"] = "application/msword",
122
["docx"] = "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
123
["eot"] = "application/vnd.ms-fontobject",
124
["epub"] = "application/epub+zip",
125
["gz"] = "application/gzip",
126
["gif"] = "image/gif",
127
["htm"] = CONTENT_TEXT_HTML,
128
["html"] = CONTENT_TEXT_HTML,
129
["ico"] = "image/vnd.microsoft.icon",
130
["ics"] = "text/calendar",
131
["jar"] = "application/java-archive",
132
["jpeg"] = "image/jpeg",
133
["json"] = "application/json",
134
["jsonld"] = "application/ld+json",
135
["mid"] = "audio/x-midi",
136
["mjs"] = "text/javascript",
137
["mp3"] = "audio/mpeg",
138
["mp4"] = "video/mp4",
139
["mpeg"] = "video/mpeg",
140
["mpkg"] = "application/vnd.apple.installer+xml",
141
["odp"] = "application/vnd.oasis.opendocument.presentation",
142
["ods"] = "application/vnd.oasis.opendocument.spreadsheet",
143
["odt"] = "application/vnd.oasis.opendocument.text",
144
["oga"] = "audio/ogg",
145
["ogv"] = "video/ogg",
146
["ogx"] = "application/ogg",
147
["opus"] = "audio/opus",
148
["otf"] = "font/otf",
149
["png"] = "image/png",
150
["pdf"] = "application/pdf",
151
["php"] = "application/x-httpd-php",
152
["ppt"] = "application/vnd.ms-powerpoint",
153
["pptx"] = "application/vnd.openxmlformats-officedocument.presentationml.presentation",
154
["rar"] = "application/vnd.rar",
155
["rtf"] = "application/rtf",
156
["sh"] = "application/x-sh",
157
["svg"] = "image/svg+xml",
158
["tar"] = "application/x-tar",
159
["tif"] = "image/tiff",
160
["tiff"] = "image/tiff",
161
["ts"] = "video/mp2t",
162
["ttf"] = "font/ttf",
163
["txt"] = "text/plain",
164
["vsd"] = "application/vnd.visio",
165
["wav"] = "audio/wav",
166
["weba"] = "audio/webm",
167
["webm"] = "video/webm",
168
["webp"] = "image/webp",
169
["woff"] = "font/woff",
170
["woff2"] = "font/woff2",
171
["xhtml"] = "application/xhtml+xml",
172
["xls"] = "application/vnd.ms-excel",
173
["xlsx"] = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
174
["xml"] = "default.",
175
["xul"] = "application/vnd.mozilla.xul+xml",
176
["zip"] = "application/zip",
177
["3gp"] = "video",
178
["3g2"] = "video",
179
["7z"] = "application/x-7z-compressed",
180
}
181
182
--[[
183
builtin dynamic pages
184
--]]
185
local DYNAMIC_PAGES = {
186
187
-- main home page
188
["/"] = [[
189
<!doctype html>
190
<html lang="en">
191
192
<head>
193
<meta charset="utf-8">
194
<title>ArduPilot</title>
195
<script>
196
<?lstr JS_LIBRARY['dynamic_load']?>
197
</script>
198
</head>
199
200
<h2>ArduPilot PPP Gateway</h2>
201
<body onload="javascript: dynamic_load('board_status','/@DYNAMIC/board_status.shtml',1000)">
202
203
<div id="main">
204
<ul>
205
<li><a href="mnt/">Filesystem Access</a></li>
206
<li><a href="?FWUPDATE">Reboot for Firmware Update</a></li>
207
</ul>
208
</div>
209
<h2>Controller Status</h2>
210
<div id="board_status"></div>
211
</body>
212
</html>
213
]],
214
215
-- board status section on front page
216
["@DYNAMIC/board_status.shtml"] = [[
217
<table>
218
<tr><td>Firmware</td><td><?lstr FWVersion:string() ?></td></tr>
219
<tr><td>GIT Hash</td><td><?lstr FWVersion:hash() ?></td></tr>
220
<tr><td>Uptime</td><td><?lstr hms_uptime() ?></td></tr>
221
<tr><td>IP</td><td><?lstr networking:address_to_str(networking:get_ip_active()) ?></td></tr>
222
<tr><td>Netmask</td><td><?lstr networking:address_to_str(networking:get_netmask_active()) ?></td></tr>
223
<tr><td>Gateway</td><td><?lstr networking:address_to_str(networking:get_gateway_active()) ?></td></tr>
224
<tr><td>MCU Temperature</td><td><?lstr string.format("%.1fC", analog:mcu_temperature()) ?></td></tr>
225
</table>
226
]]
227
}
228
229
reboot_counter = 0
230
231
local ACTION_PAGES = {
232
["/?FWUPDATE"] = function()
233
periph:can_printf("Rebooting for firmware update")
234
reboot_counter = 50
235
end
236
}
237
238
--[[
239
builtin javascript library functions
240
--]]
241
JS_LIBRARY = {
242
["dynamic_load"] = [[
243
function dynamic_load(div_id, uri, period_ms) {
244
var xhr = new XMLHttpRequest();
245
xhr.open('GET', uri);
246
247
xhr.setRequestHeader("Cache-Control", "no-cache, no-store, max-age=0");
248
xhr.setRequestHeader("Expires", "Tue, 01 Jan 1980 1:00:00 GMT");
249
xhr.setRequestHeader("Pragma", "no-cache");
250
251
xhr.onload = function () {
252
if (xhr.status === 200) {
253
var output = document.getElementById(div_id);
254
if (uri.endsWith('.shtml') || uri.endsWith('.html')) {
255
output.innerHTML = xhr.responseText;
256
} else {
257
output.textContent = xhr.responseText;
258
}
259
}
260
setTimeout(function() { dynamic_load(div_id,uri, period_ms); }, period_ms);
261
}
262
xhr.send();
263
}
264
]]
265
}
266
267
if not sock_listen:bind("0.0.0.0", WEB_BIND_PORT:get()) then
268
periph:can_printf(string.format("WebServer: failed to bind to TCP %u", WEB_BIND_PORT:get()))
269
return
270
end
271
272
if not sock_listen:listen(20) then
273
periph:can_printf("WebServer: failed to listen")
274
return
275
end
276
277
function hms_uptime()
278
local s = (millis()/1000):toint()
279
local min = math.floor(s / 60) % 60
280
local hr = math.floor(s / 3600)
281
return string.format("%u hours %u minutes %u seconds", hr, min, s%60)
282
end
283
284
--[[
285
split string by pattern
286
--]]
287
local function split(str, pattern)
288
local ret = {}
289
for s in string.gmatch(str, pattern) do
290
table.insert(ret, s)
291
end
292
return ret
293
end
294
295
--[[
296
return true if a string ends in the 2nd string
297
--]]
298
local function endswith(str, s)
299
local len1 = #str
300
local len2 = #s
301
return string.sub(str,1+len1-len2,len1) == s
302
end
303
304
--[[
305
return true if a string starts with the 2nd string
306
--]]
307
local function startswith(str, s)
308
return string.sub(str,1,#s) == s
309
end
310
311
local debug_count=0
312
313
function DEBUG(txt)
314
if WEB_DEBUG:get() ~= 0 then
315
periph:can_printf(txt .. string.format(" [%u]", debug_count))
316
debug_count = debug_count + 1
317
end
318
end
319
320
--[[
321
return index of element in a table
322
--]]
323
function table_index(t,el)
324
for i,v in ipairs(t) do
325
if v == el then
326
return i
327
end
328
end
329
return nil
330
end
331
332
--[[
333
return true if a table contains a given element
334
--]]
335
function table_contains(t,el)
336
local i = table_index(t, el)
337
return i ~= nil
338
end
339
340
function is_hidden_dir(path)
341
return table_contains(HIDDEN_FOLDERS, path)
342
end
343
344
local DAYS = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" }
345
local MONTHS = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }
346
347
function isdirectory(path)
348
local s = fs:stat(path)
349
return s and s:is_directory()
350
end
351
352
--[[
353
time string for directory listings
354
--]]
355
function file_timestring(path)
356
local s = fs:stat(path)
357
if not s then
358
return ""
359
end
360
local mtime = s:mtime()
361
local year, month, day, hour, min, sec, _ = rtc:clock_s_to_date_fields(mtime)
362
if not year then
363
return ""
364
end
365
return string.format("%04u-%02u-%02u %02u:%02u", year, month+1, day, hour, min, sec)
366
end
367
368
--[[
369
time string for Last-Modified
370
--]]
371
function file_timestring_http(mtime)
372
local year, month, day, hour, min, sec, wday = rtc:clock_s_to_date_fields(mtime)
373
if not year then
374
return ""
375
end
376
return string.format("%s, %02u %s %u %02u:%02u:%02u GMT",
377
DAYS[wday+1],
378
day,
379
MONTHS[month+1],
380
year,
381
hour,
382
min,
383
sec)
384
end
385
386
--[[
387
parse a http time string to a uint32_t seconds timestamp
388
--]]
389
function file_timestring_http_parse(tstring)
390
local dayname, day, monthname, year, hour, min, sec =
391
string.match(tstring,
392
'(%w%w%w), (%d+) (%w%w%w) (%d%d%d%d) (%d%d):(%d%d):(%d%d) GMT')
393
if not dayname then
394
return nil
395
end
396
local mon = table_index(MONTHS, monthname)
397
return rtc:date_fields_to_clock_s(year, mon-1, day, hour, min, sec)
398
end
399
400
--[[
401
return true if path exists and is not a directory
402
--]]
403
function file_exists(path)
404
local s = fs:stat(path)
405
if not s then
406
return false
407
end
408
return not s:is_directory()
409
end
410
411
--[[
412
substitute variables of form {xxx} from a table
413
from http://lua-users.org/wiki/StringInterpolation
414
--]]
415
function substitute_vars(s, vars)
416
s = (string.gsub(s, "({([^}]+)})",
417
function(whole,i)
418
return vars[i] or whole
419
end))
420
return s
421
end
422
423
--[[
424
lat or lon as a string, working around limited type in ftoa_engine
425
--]]
426
function latlon_str(ll)
427
local ipart = tonumber(string.match(tostring(ll*1.0e-7), '(.*[.]).*'))
428
local fpart = math.abs(ll - ipart*10000000)
429
return string.format("%d.%u", ipart, fpart, ipart*10000000, ll)
430
end
431
432
--[[
433
location string for home page
434
--]]
435
function location_string(loc)
436
return substitute_vars([[<a href="https://www.google.com/maps/search/?api=1&query={lat},{lon}" target="_blank">{lat} {lon}</a> {alt}]],
437
{ ["lat"] = latlon_str(loc:lat()),
438
["lon"] = latlon_str(loc:lng()),
439
["alt"] = string.format("%.1fm", loc:alt()*1.0e-2) })
440
end
441
442
--[[
443
client class for open connections
444
--]]
445
local function Client(sock, idx)
446
local self = {}
447
448
self.closed = false
449
450
local have_header = false
451
local header = ""
452
local header_lines = {}
453
local header_vars = {}
454
local run = nil
455
local protocol = nil
456
local file = nil
457
local start_time = millis()
458
local offset = 0
459
460
function self.read_header()
461
local s = sock:recv(2048)
462
if not s then
463
local now = millis()
464
if not sock:is_connected() or now - start_time > WEB_TIMEOUT:get()*1000 then
465
-- EOF while looking for header
466
DEBUG(string.format("%u: EOF", idx))
467
self.remove()
468
return false
469
end
470
return false
471
end
472
if not s or #s == 0 then
473
return false
474
end
475
header = header .. s
476
local eoh = string.find(s, '\r\n\r\n')
477
if eoh then
478
DEBUG(string.format("%u: got header", idx))
479
have_header = true
480
header_lines = split(header, "[^\r\n]+")
481
-- blocking for reply
482
sock:set_blocking(true)
483
return true
484
end
485
return false
486
end
487
488
function self.sendstring(s)
489
sock:send(s, #s)
490
end
491
492
function self.sendline(s)
493
self.sendstring(s .. "\r\n")
494
end
495
496
--[[
497
send a string with variable substitution using {varname}
498
--]]
499
function self.sendstring_vars(s, vars)
500
self.sendstring(substitute_vars(s, vars))
501
end
502
503
function self.send_header(code, codestr, vars)
504
self.sendline(string.format("%s %u %s", protocol, code, codestr))
505
self.sendline(string.format("Server: %s", SERVER_VERSION))
506
for k,v in pairs(vars) do
507
self.sendline(string.format("%s: %s", k, v))
508
end
509
self.sendline("Connection: close")
510
self.sendline("")
511
end
512
513
-- get size of a file
514
function self.file_size(fname)
515
local s = fs:stat(fname)
516
if not s then
517
return 0
518
end
519
local ret = s:size():toint()
520
DEBUG(string.format("%u: size of '%s' -> %u", idx, fname, ret))
521
return ret
522
end
523
524
525
--[[
526
return full path with .. resolution
527
--]]
528
function self.full_path(path, name)
529
DEBUG(string.format("%u: full_path(%s,%s)", idx, path, name))
530
local ret = path
531
if path == "/" and startswith(name,"@") then
532
return name
533
end
534
if name == ".." then
535
if path == "/" then
536
return "/"
537
end
538
if endswith(path,"/") then
539
path = string.sub(path, 1, #path-1)
540
end
541
local dir, _ = string.match(path, '(.*/)(.*)')
542
if not dir then
543
return path
544
end
545
return dir
546
end
547
if not endswith(ret, "/") then
548
ret = ret .. "/"
549
end
550
ret = ret .. name
551
DEBUG(string.format("%u: full_path(%s,%s) -> %s", idx, path, name, ret))
552
return ret
553
end
554
555
function self.directory_list(path)
556
sock:set_blocking(true)
557
if startswith(path, "/@") then
558
path = string.sub(path, 2, #path-1)
559
end
560
DEBUG(string.format("%u: directory_list(%s)", idx, path))
561
local dlist = dirlist(path)
562
if not dlist then
563
dlist = {}
564
end
565
if not table_contains(dlist, "..") then
566
-- on ChibiOS we don't get ..
567
table.insert(dlist, "..")
568
end
569
if path == "/" then
570
for _,v in ipairs(HIDDEN_FOLDERS) do
571
table.insert(dlist, v)
572
end
573
end
574
575
table.sort(dlist)
576
self.send_header(200, "OK", {["Content-Type"]=CONTENT_TEXT_HTML})
577
self.sendline(DOCTYPE)
578
self.sendstring_vars([[
579
<html>
580
<head>
581
<title>Index of {path}</title>
582
</head>
583
<body>
584
<h1>Index of {path}</h1>
585
<table>
586
<tr><th align="left">Name</th><th align="left">Last modified</th><th align="left">Size</th></tr>
587
]], {path=path})
588
for _,d in ipairs(dlist) do
589
local skip = d == "."
590
if not skip then
591
local fullpath = self.full_path(path, d)
592
local name = d
593
local sizestr = "0"
594
local stat = fs:stat(fullpath)
595
local size = stat and stat:size() or 0
596
if is_hidden_dir(fullpath) or (stat and stat:is_directory()) then
597
name = name .. "/"
598
elseif size >= 100*1000*1000 then
599
sizestr = string.format("%uM", (size/(1000*1000)):toint())
600
else
601
sizestr = tostring(size)
602
end
603
local modtime = file_timestring(fullpath)
604
self.sendstring_vars([[<tr><td align="left"><a href="{name}">{name}</a></td><td align="left">{modtime}</td><td align="left">{size}</td></tr>
605
]], { name=name, size=sizestr, modtime=modtime })
606
end
607
end
608
self.sendstring([[
609
</table>
610
</body>
611
</html>
612
]])
613
end
614
615
-- send file content
616
function self.send_file()
617
if not sock:pollout(0) then
618
return
619
end
620
local chunk = WEB_BLOCK_SIZE:get()
621
local b = file:read(chunk)
622
sock:set_blocking(true)
623
if b and #b > 0 then
624
local sent = sock:send(b, #b)
625
if sent == -1 then
626
run = nil
627
self.remove()
628
return
629
end
630
if sent < #b then
631
file:seek(offset+sent)
632
end
633
offset = offset + sent
634
end
635
if not b or #b < chunk then
636
-- EOF
637
DEBUG(string.format("%u: sent file", idx))
638
run = nil
639
self.remove()
640
return
641
end
642
end
643
644
--[[
645
load whole file as a string
646
--]]
647
function self.load_file()
648
local chunk = WEB_BLOCK_SIZE:get()
649
local ret = ""
650
while true do
651
local b = file:read(chunk)
652
if not b or #b == 0 then
653
break
654
end
655
ret = ret .. b
656
end
657
return ret
658
end
659
660
--[[
661
evaluate some lua code and return as a string
662
--]]
663
function self.evaluate(code)
664
local eval_code = "function eval_func()\n" .. code .. "\nend\n"
665
local f, errloc, err = load(eval_code, "eval_func", "t", _ENV)
666
if not f then
667
DEBUG(string.format("load failed: err=%s errloc=%s", err, errloc))
668
return nil
669
end
670
local success, err2 = pcall(f)
671
if not success then
672
DEBUG(string.format("pcall failed: err=%s", err2))
673
return nil
674
end
675
local ok, s2 = pcall(eval_func)
676
eval_func = nil
677
if ok then
678
return s2
679
end
680
return nil
681
end
682
683
--[[
684
process a file as a lua CGI
685
--]]
686
function self.send_cgi()
687
sock:set_blocking(true)
688
local contents = self.load_file()
689
local s = self.evaluate(contents)
690
if s then
691
self.sendstring(s)
692
end
693
self.remove()
694
end
695
696
--[[
697
send file content with server side processsing
698
files ending in .shtml can have embedded lua lika this:
699
<?lua return "foo" ?>
700
<?lstr 2.6+7.2 ?>
701
702
Using 'lstr' a return tostring(yourcode) is added to the code
703
automatically
704
--]]
705
function self.send_processed_file(dynamic_page)
706
sock:set_blocking(true)
707
local contents
708
if dynamic_page then
709
contents = file
710
else
711
contents = self.load_file()
712
end
713
while #contents > 0 do
714
local pat1 = "(.-)[<][?]lua[ \n](.-)[?][>](.*)"
715
local pat2 = "(.-)[<][?]lstr[ \n](.-)[?][>](.*)"
716
local p1, p2, p3 = string.match(contents, pat1)
717
if not p1 then
718
p1, p2, p3 = string.match(contents, pat2)
719
if not p1 then
720
break
721
end
722
p2 = "return tostring(" .. p2 .. ")"
723
end
724
self.sendstring(p1)
725
local s2 = self.evaluate(p2)
726
if s2 then
727
self.sendstring(s2)
728
end
729
contents = p3
730
end
731
self.sendstring(contents)
732
self.remove()
733
end
734
735
-- return a content type
736
function self.content_type(path)
737
if path == "/" then
738
return MIME_TYPES["html"]
739
end
740
local _, ext = string.match(path, '(.*[.])(.*)')
741
ext = string.lower(ext)
742
local ret = MIME_TYPES[ext]
743
if not ret then
744
return CONTENT_OCTET_STREAM
745
end
746
return ret
747
end
748
749
-- perform a file download
750
function self.file_download(path)
751
if startswith(path, "/@") then
752
path = string.sub(path, 2, #path)
753
end
754
DEBUG(string.format("%u: file_download(%s)", idx, path))
755
file = DYNAMIC_PAGES[path]
756
dynamic_page = file ~= nil
757
if not dynamic_page then
758
file = io.open(path,"rb")
759
if not file then
760
DEBUG(string.format("%u: Failed to open '%s'", idx, path))
761
return false
762
end
763
end
764
local vars = {["Content-Type"]=self.content_type(path)}
765
local cgi_processing = startswith(path, "/cgi-bin/") and endswith(path, ".lua")
766
local server_side_processing = endswith(path, ".shtml")
767
local stat = fs:stat(path)
768
if not startswith(path, "@") and
769
not server_side_processing and
770
not cgi_processing and stat and
771
not dynamic_page then
772
local fsize = stat:size()
773
local mtime = stat:mtime()
774
vars["Content-Length"]= tostring(fsize)
775
local modtime = file_timestring_http(mtime)
776
if modtime then
777
vars["Last-Modified"] = modtime
778
end
779
local if_modified_since = header_vars['If-Modified-Since']
780
if if_modified_since then
781
local tsec = file_timestring_http_parse(if_modified_since)
782
if tsec and tsec >= mtime then
783
DEBUG(string.format("%u: Not modified: %s %s", idx, modtime, if_modified_since))
784
self.send_header(304, "Not Modified", vars)
785
return true
786
end
787
end
788
end
789
self.send_header(200, "OK", vars)
790
if server_side_processing or dynamic_page then
791
DEBUG(string.format("%u: shtml processing %s", idx, path))
792
run = self.send_processed_file(dynamic_page)
793
elseif cgi_processing then
794
DEBUG(string.format("%u: CGI processing %s", idx, path))
795
run = self.send_cgi
796
elseif stat and
797
WEB_SENDFILE_MIN:get() > 0 and
798
stat:size() >= WEB_SENDFILE_MIN:get() and
799
sock:sendfile(file) then
800
return true
801
else
802
run = self.send_file
803
end
804
return true
805
end
806
807
function self.not_found()
808
self.send_header(404, "Not found", {})
809
end
810
811
function self.moved_permanently(relpath)
812
if not startswith(relpath, "/") then
813
relpath = "/" .. relpath
814
end
815
local location = string.format("http://%s%s", header_vars['Host'], relpath)
816
DEBUG(string.format("%u: Redirect -> %s", idx, location))
817
self.send_header(301, "Moved Permanently", {["Location"]=location})
818
end
819
820
-- process a single request
821
function self.process_request()
822
local h1 = header_lines[1]
823
if not h1 or #h1 == 0 then
824
DEBUG(string.format("%u: empty request", idx))
825
return
826
end
827
local cmd = split(header_lines[1], "%S+")
828
if not cmd or #cmd < 3 then
829
DEBUG(string.format("bad request: %s", header_lines[1]))
830
return
831
end
832
if cmd[1] ~= "GET" then
833
DEBUG(string.format("bad op: %s", cmd[1]))
834
return
835
end
836
protocol = cmd[3]
837
if protocol ~= "HTTP/1.0" and protocol ~= "HTTP/1.1" then
838
DEBUG(string.format("bad protocol: %s", protocol))
839
return
840
end
841
local path = cmd[2]
842
DEBUG(string.format("%u: path='%s'", idx, path))
843
844
-- extract header variables
845
for i = 2,#header_lines do
846
local key, var = string.match(header_lines[i], '(.*): (.*)')
847
if key then
848
header_vars[key] = var
849
end
850
end
851
852
if ACTION_PAGES[path] ~= nil then
853
DEBUG(string.format("Running ACTION %s", path))
854
local fn = ACTION_PAGES[path]
855
self.send_header(200, "OK", {["Content-Type"]=CONTENT_TEXT_HTML})
856
self.sendstring([[
857
<html>
858
<head>
859
<meta http-equiv="refresh" content="2; url=/">
860
</head>
861
</html>
862
]])
863
fn()
864
return
865
end
866
867
if DYNAMIC_PAGES[path] ~= nil then
868
self.file_download(path)
869
return
870
end
871
872
if path == MNT_PREFIX then
873
path = "/"
874
end
875
if startswith(path, MNT_PREFIX2) then
876
path = string.sub(path,#MNT_PREFIX2,#path)
877
end
878
879
if isdirectory(path) and
880
not endswith(path,"/") and
881
header_vars['Host'] and
882
not is_hidden_dir(path) then
883
self.moved_permanently(path .. "/")
884
return
885
end
886
887
if path ~= "/" and endswith(path,"/") then
888
path = string.sub(path, 1, #path-1)
889
end
890
891
if startswith(path,"/@") then
892
path = string.sub(path, 2, #path)
893
end
894
895
-- see if we have an index file
896
if isdirectory(path) and file_exists(path .. "/index.html") then
897
DEBUG(string.format("%u: found index.html", idx))
898
if self.file_download(path .. "/index.html") then
899
return
900
end
901
end
902
903
-- see if it is a directory
904
if (path == "/" or
905
DYNAMIC_PAGES[path] == nil) and
906
(endswith(path,"/") or
907
isdirectory(path) or
908
is_hidden_dir(path)) then
909
self.directory_list(path)
910
return
911
end
912
913
-- or a file
914
if self.file_download(path) then
915
return
916
end
917
self.not_found(path)
918
end
919
920
-- update the client
921
function self.update()
922
if run then
923
run()
924
return
925
end
926
if not have_header then
927
if not self.read_header() then
928
return
929
end
930
end
931
self.process_request()
932
if not run then
933
-- nothing more to do
934
self.remove()
935
end
936
end
937
938
function self.remove()
939
DEBUG(string.format("%u: removing client OFFSET=%u", idx, offset))
940
if self.closed then
941
return
942
end
943
sock:close()
944
self.closed = true
945
end
946
947
-- return the instance
948
return self
949
end
950
951
--[[
952
see if any new clients want to connect
953
--]]
954
local function check_new_clients()
955
while sock_listen:pollin(0) do
956
local sock = sock_listen:accept()
957
if not sock then
958
return
959
end
960
-- non-blocking for header read
961
sock:set_blocking(false)
962
-- find free client slot
963
for i = 1, #clients+1 do
964
if clients[i] == nil then
965
local idx = i
966
local client = Client(sock, idx)
967
DEBUG(string.format("%u: New client", idx))
968
clients[idx] = client
969
end
970
end
971
end
972
end
973
974
--[[
975
check for client activity
976
--]]
977
local function check_clients()
978
for idx,client in ipairs(clients) do
979
if not client.closed then
980
client.update()
981
end
982
if client.closed then
983
table.remove(clients,idx)
984
end
985
end
986
end
987
988
local function update()
989
check_new_clients()
990
check_clients()
991
if reboot_counter then
992
reboot_counter = reboot_counter - 1
993
if reboot_counter == 0 then
994
periph:can_printf("Rebooting")
995
periph:reboot(true)
996
end
997
end
998
return update,5
999
end
1000
1001
return update,100
1002
1003