Path: blob/master/Tools/AP_Periph/Web/scripts/pppgw_webui.lua
9354 views
--[[1example script to test lua socket API2--]]34---@diagnostic disable: param-type-mismatch5---@diagnostic disable: need-check-nil6---@diagnostic disable: redundant-parameter7---@diagnostic disable: undefined-field89PARAM_TABLE_KEY = 4710PARAM_TABLE_PREFIX = "WEB_"1112-- add a parameter and bind it to a variable13function bind_add_param(name, idx, default_value)14assert(param:add_param(PARAM_TABLE_KEY, idx, name, default_value), string.format('could not add param %s', name))15return Parameter(PARAM_TABLE_PREFIX .. name)16end1718-- Setup Parameters19assert(param:add_table(PARAM_TABLE_KEY, PARAM_TABLE_PREFIX, 6), 'net_test: could not add param table')2021--[[22// @Param: WEB_ENABLE23// @DisplayName: enable web server24// @Description: enable web server25// @Values: 0:Disabled,1:Enabled26// @User: Standard27--]]28local WEB_ENABLE = bind_add_param('ENABLE', 1, 1)2930--[[31// @Param: WEB_BIND_PORT32// @DisplayName: web server TCP port33// @Description: web server TCP port34// @Range: 1 6553535// @User: Standard36--]]37local WEB_BIND_PORT = bind_add_param('BIND_PORT', 2, 80)3839--[[40// @Param: WEB_DEBUG41// @DisplayName: web server debugging42// @Description: web server debugging43// @Values: 0:Disabled,1:Enabled44// @User: Advanced45--]]46local WEB_DEBUG = bind_add_param('DEBUG', 3, 0)4748--[[49// @Param: WEB_BLOCK_SIZE50// @DisplayName: web server block size51// @Description: web server block size for download52// @Range: 1 6553553// @User: Advanced54--]]55local WEB_BLOCK_SIZE = bind_add_param('BLOCK_SIZE', 4, 10240)5657--[[58// @Param: WEB_TIMEOUT59// @DisplayName: web server timeout60// @Description: timeout for inactive connections61// @Units: s62// @Range: 0.1 6063// @User: Advanced64--]]65local WEB_TIMEOUT = bind_add_param('TIMEOUT', 5, 2.0)6667--[[68// @Param: WEB_SENDFILE_MIN69// @DisplayName: web server minimum file size for sendfile70// @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 download71// @Range: 0 1000000072// @User: Advanced73--]]74local WEB_SENDFILE_MIN = bind_add_param('SENDFILE_MIN', 6, 100000)7576if WEB_ENABLE:get() ~= 1 then77periph:can_printf("WebServer: disabled")78return79end8081periph:can_printf(string.format("WebServer: starting on port %u", WEB_BIND_PORT:get()))8283local sock_listen = Socket(0)84local clients = {}8586local DOCTYPE = "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 3.2 Final//EN\">"87local SERVER_VERSION = "net_webserver 1.0"88local CONTENT_TEXT_HTML = "text/html;charset=UTF-8"89local CONTENT_OCTET_STREAM = "application/octet-stream"9091local HIDDEN_FOLDERS = { "@SYS", "@ROMFS", "@MISSION", "@PARAM" }9293local MNT_PREFIX = "/mnt"94local MNT_PREFIX2 = MNT_PREFIX .. "/"9596local MIME_TYPES = {97["apj"] = CONTENT_OCTET_STREAM,98["dat"] = CONTENT_OCTET_STREAM,99["o"] = CONTENT_OCTET_STREAM,100["obj"] = CONTENT_OCTET_STREAM,101["lua"] = "text/x-lua",102["py"] = "text/x-python",103["shtml"] = CONTENT_TEXT_HTML,104["js"] = "text/javascript",105-- thanks to https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types106["aac"] = "audio/aac",107["abw"] = "application/x-abiword",108["arc"] = "application/x-freearc",109["avif"] = "image/avif",110["avi"] = "video/x-msvideo",111["azw"] = "application/vnd.amazon.ebook",112["bin"] = "application/octet-stream",113["bmp"] = "image/bmp",114["bz"] = "application/x-bzip",115["bz2"] = "application/x-bzip2",116["cda"] = "application/x-cdf",117["csh"] = "application/x-csh",118["css"] = "text/css",119["csv"] = "text/csv",120["doc"] = "application/msword",121["docx"] = "application/vnd.openxmlformats-officedocument.wordprocessingml.document",122["eot"] = "application/vnd.ms-fontobject",123["epub"] = "application/epub+zip",124["gz"] = "application/gzip",125["gif"] = "image/gif",126["htm"] = CONTENT_TEXT_HTML,127["html"] = CONTENT_TEXT_HTML,128["ico"] = "image/vnd.microsoft.icon",129["ics"] = "text/calendar",130["jar"] = "application/java-archive",131["jpeg"] = "image/jpeg",132["json"] = "application/json",133["jsonld"] = "application/ld+json",134["mid"] = "audio/x-midi",135["mjs"] = "text/javascript",136["mp3"] = "audio/mpeg",137["mp4"] = "video/mp4",138["mpeg"] = "video/mpeg",139["mpkg"] = "application/vnd.apple.installer+xml",140["odp"] = "application/vnd.oasis.opendocument.presentation",141["ods"] = "application/vnd.oasis.opendocument.spreadsheet",142["odt"] = "application/vnd.oasis.opendocument.text",143["oga"] = "audio/ogg",144["ogv"] = "video/ogg",145["ogx"] = "application/ogg",146["opus"] = "audio/opus",147["otf"] = "font/otf",148["png"] = "image/png",149["pdf"] = "application/pdf",150["php"] = "application/x-httpd-php",151["ppt"] = "application/vnd.ms-powerpoint",152["pptx"] = "application/vnd.openxmlformats-officedocument.presentationml.presentation",153["rar"] = "application/vnd.rar",154["rtf"] = "application/rtf",155["sh"] = "application/x-sh",156["svg"] = "image/svg+xml",157["tar"] = "application/x-tar",158["tif"] = "image/tiff",159["tiff"] = "image/tiff",160["ts"] = "video/mp2t",161["ttf"] = "font/ttf",162["txt"] = "text/plain",163["vsd"] = "application/vnd.visio",164["wav"] = "audio/wav",165["weba"] = "audio/webm",166["webm"] = "video/webm",167["webp"] = "image/webp",168["woff"] = "font/woff",169["woff2"] = "font/woff2",170["xhtml"] = "application/xhtml+xml",171["xls"] = "application/vnd.ms-excel",172["xlsx"] = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",173["xml"] = "default.",174["xul"] = "application/vnd.mozilla.xul+xml",175["zip"] = "application/zip",176["3gp"] = "video",177["3g2"] = "video",178["7z"] = "application/x-7z-compressed",179}180181--[[182builtin dynamic pages183--]]184local DYNAMIC_PAGES = {185186-- main home page187["/"] = [[188<!doctype html>189<html lang="en">190191<head>192<meta charset="utf-8">193<title>ArduPilot</title>194<script>195<?lstr JS_LIBRARY['dynamic_load']?>196</script>197</head>198199<h2>ArduPilot PPP Gateway</h2>200<body onload="javascript: dynamic_load('board_status','/@DYNAMIC/board_status.shtml',1000)">201202<div id="main">203<ul>204<li><a href="mnt/">Filesystem Access</a></li>205<li><a href="?FWUPDATE">Reboot for Firmware Update</a></li>206</ul>207</div>208<h2>Controller Status</h2>209<div id="board_status"></div>210</body>211</html>212]],213214-- board status section on front page215["@DYNAMIC/board_status.shtml"] = [[216<table>217<tr><td>Firmware</td><td><?lstr FWVersion:string() ?></td></tr>218<tr><td>GIT Hash</td><td><?lstr FWVersion:hash() ?></td></tr>219<tr><td>Uptime</td><td><?lstr hms_uptime() ?></td></tr>220<tr><td>IP</td><td><?lstr networking:address_to_str(networking:get_ip_active()) ?></td></tr>221<tr><td>Netmask</td><td><?lstr networking:address_to_str(networking:get_netmask_active()) ?></td></tr>222<tr><td>Gateway</td><td><?lstr networking:address_to_str(networking:get_gateway_active()) ?></td></tr>223<tr><td>MCU Temperature</td><td><?lstr string.format("%.1fC", analog:mcu_temperature()) ?></td></tr>224</table>225]]226}227228reboot_counter = 0229230local ACTION_PAGES = {231["/?FWUPDATE"] = function()232periph:can_printf("Rebooting for firmware update")233reboot_counter = 50234end235}236237--[[238builtin javascript library functions239--]]240JS_LIBRARY = {241["dynamic_load"] = [[242function dynamic_load(div_id, uri, period_ms) {243var xhr = new XMLHttpRequest();244xhr.open('GET', uri);245246xhr.setRequestHeader("Cache-Control", "no-cache, no-store, max-age=0");247xhr.setRequestHeader("Expires", "Tue, 01 Jan 1980 1:00:00 GMT");248xhr.setRequestHeader("Pragma", "no-cache");249250xhr.onload = function () {251if (xhr.status === 200) {252var output = document.getElementById(div_id);253if (uri.endsWith('.shtml') || uri.endsWith('.html')) {254output.innerHTML = xhr.responseText;255} else {256output.textContent = xhr.responseText;257}258}259setTimeout(function() { dynamic_load(div_id,uri, period_ms); }, period_ms);260}261xhr.send();262}263]]264}265266if not sock_listen:bind("0.0.0.0", WEB_BIND_PORT:get()) then267periph:can_printf(string.format("WebServer: failed to bind to TCP %u", WEB_BIND_PORT:get()))268return269end270271if not sock_listen:listen(20) then272periph:can_printf("WebServer: failed to listen")273return274end275276function hms_uptime()277local s = (millis()/1000):toint()278local min = math.floor(s / 60) % 60279local hr = math.floor(s / 3600)280return string.format("%u hours %u minutes %u seconds", hr, min, s%60)281end282283--[[284split string by pattern285--]]286local function split(str, pattern)287local ret = {}288for s in string.gmatch(str, pattern) do289table.insert(ret, s)290end291return ret292end293294--[[295return true if a string ends in the 2nd string296--]]297local function endswith(str, s)298local len1 = #str299local len2 = #s300return string.sub(str,1+len1-len2,len1) == s301end302303--[[304return true if a string starts with the 2nd string305--]]306local function startswith(str, s)307return string.sub(str,1,#s) == s308end309310local debug_count=0311312function DEBUG(txt)313if WEB_DEBUG:get() ~= 0 then314periph:can_printf(txt .. string.format(" [%u]", debug_count))315debug_count = debug_count + 1316end317end318319--[[320return index of element in a table321--]]322function table_index(t,el)323for i,v in ipairs(t) do324if v == el then325return i326end327end328return nil329end330331--[[332return true if a table contains a given element333--]]334function table_contains(t,el)335local i = table_index(t, el)336return i ~= nil337end338339function is_hidden_dir(path)340return table_contains(HIDDEN_FOLDERS, path)341end342343local DAYS = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" }344local MONTHS = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }345346function isdirectory(path)347local s = fs:stat(path)348return s and s:is_directory()349end350351--[[352time string for directory listings353--]]354function file_timestring(path)355local s = fs:stat(path)356if not s then357return ""358end359local mtime = s:mtime()360local year, month, day, hour, min, sec, _ = rtc:clock_s_to_date_fields(mtime)361if not year then362return ""363end364return string.format("%04u-%02u-%02u %02u:%02u", year, month+1, day, hour, min, sec)365end366367--[[368time string for Last-Modified369--]]370function file_timestring_http(mtime)371local year, month, day, hour, min, sec, wday = rtc:clock_s_to_date_fields(mtime)372if not year then373return ""374end375return string.format("%s, %02u %s %u %02u:%02u:%02u GMT",376DAYS[wday+1],377day,378MONTHS[month+1],379year,380hour,381min,382sec)383end384385--[[386parse a http time string to a uint32_t seconds timestamp387--]]388function file_timestring_http_parse(tstring)389local dayname, day, monthname, year, hour, min, sec =390string.match(tstring,391'(%w%w%w), (%d+) (%w%w%w) (%d%d%d%d) (%d%d):(%d%d):(%d%d) GMT')392if not dayname then393return nil394end395local mon = table_index(MONTHS, monthname)396return rtc:date_fields_to_clock_s(year, mon-1, day, hour, min, sec)397end398399--[[400return true if path exists and is not a directory401--]]402function file_exists(path)403local s = fs:stat(path)404if not s then405return false406end407return not s:is_directory()408end409410--[[411substitute variables of form {xxx} from a table412from http://lua-users.org/wiki/StringInterpolation413--]]414function substitute_vars(s, vars)415s = (string.gsub(s, "({([^}]+)})",416function(whole,i)417return vars[i] or whole418end))419return s420end421422--[[423lat or lon as a string, working around limited type in ftoa_engine424--]]425function latlon_str(ll)426local ipart = tonumber(string.match(tostring(ll*1.0e-7), '(.*[.]).*'))427local fpart = math.abs(ll - ipart*10000000)428return string.format("%d.%u", ipart, fpart, ipart*10000000, ll)429end430431--[[432location string for home page433--]]434function location_string(loc)435return substitute_vars([[<a href="https://www.google.com/maps/search/?api=1&query={lat},{lon}" target="_blank">{lat} {lon}</a> {alt}]],436{ ["lat"] = latlon_str(loc:lat()),437["lon"] = latlon_str(loc:lng()),438["alt"] = string.format("%.1fm", loc:alt()*1.0e-2) })439end440441--[[442client class for open connections443--]]444local function Client(sock, idx)445local self = {}446447self.closed = false448449local have_header = false450local header = ""451local header_lines = {}452local header_vars = {}453local run = nil454local protocol = nil455local file = nil456local start_time = millis()457local offset = 0458459function self.read_header()460local s = sock:recv(2048)461if not s then462local now = millis()463if not sock:is_connected() or now - start_time > WEB_TIMEOUT:get()*1000 then464-- EOF while looking for header465DEBUG(string.format("%u: EOF", idx))466self.remove()467return false468end469return false470end471if not s or #s == 0 then472return false473end474header = header .. s475local eoh = string.find(s, '\r\n\r\n')476if eoh then477DEBUG(string.format("%u: got header", idx))478have_header = true479header_lines = split(header, "[^\r\n]+")480-- blocking for reply481sock:set_blocking(true)482return true483end484return false485end486487function self.sendstring(s)488sock:send(s, #s)489end490491function self.sendline(s)492self.sendstring(s .. "\r\n")493end494495--[[496send a string with variable substitution using {varname}497--]]498function self.sendstring_vars(s, vars)499self.sendstring(substitute_vars(s, vars))500end501502function self.send_header(code, codestr, vars)503self.sendline(string.format("%s %u %s", protocol, code, codestr))504self.sendline(string.format("Server: %s", SERVER_VERSION))505for k,v in pairs(vars) do506self.sendline(string.format("%s: %s", k, v))507end508self.sendline("Connection: close")509self.sendline("")510end511512-- get size of a file513function self.file_size(fname)514local s = fs:stat(fname)515if not s then516return 0517end518local ret = s:size():toint()519DEBUG(string.format("%u: size of '%s' -> %u", idx, fname, ret))520return ret521end522523524--[[525return full path with .. resolution526--]]527function self.full_path(path, name)528DEBUG(string.format("%u: full_path(%s,%s)", idx, path, name))529local ret = path530if path == "/" and startswith(name,"@") then531return name532end533if name == ".." then534if path == "/" then535return "/"536end537if endswith(path,"/") then538path = string.sub(path, 1, #path-1)539end540local dir, _ = string.match(path, '(.*/)(.*)')541if not dir then542return path543end544return dir545end546if not endswith(ret, "/") then547ret = ret .. "/"548end549ret = ret .. name550DEBUG(string.format("%u: full_path(%s,%s) -> %s", idx, path, name, ret))551return ret552end553554function self.directory_list(path)555sock:set_blocking(true)556if startswith(path, "/@") then557path = string.sub(path, 2, #path-1)558end559DEBUG(string.format("%u: directory_list(%s)", idx, path))560local dlist = dirlist(path)561if not dlist then562dlist = {}563end564if not table_contains(dlist, "..") then565-- on ChibiOS we don't get ..566table.insert(dlist, "..")567end568if path == "/" then569for _,v in ipairs(HIDDEN_FOLDERS) do570table.insert(dlist, v)571end572end573574table.sort(dlist)575self.send_header(200, "OK", {["Content-Type"]=CONTENT_TEXT_HTML})576self.sendline(DOCTYPE)577self.sendstring_vars([[578<html>579<head>580<title>Index of {path}</title>581</head>582<body>583<h1>Index of {path}</h1>584<table>585<tr><th align="left">Name</th><th align="left">Last modified</th><th align="left">Size</th></tr>586]], {path=path})587for _,d in ipairs(dlist) do588local skip = d == "."589if not skip then590local fullpath = self.full_path(path, d)591local name = d592local sizestr = "0"593local stat = fs:stat(fullpath)594local size = stat and stat:size() or 0595if is_hidden_dir(fullpath) or (stat and stat:is_directory()) then596name = name .. "/"597elseif size >= 100*1000*1000 then598sizestr = string.format("%uM", (size/(1000*1000)):toint())599else600sizestr = tostring(size)601end602local modtime = file_timestring(fullpath)603self.sendstring_vars([[<tr><td align="left"><a href="{name}">{name}</a></td><td align="left">{modtime}</td><td align="left">{size}</td></tr>604]], { name=name, size=sizestr, modtime=modtime })605end606end607self.sendstring([[608</table>609</body>610</html>611]])612end613614-- send file content615function self.send_file()616if not sock:pollout(0) then617return618end619local chunk = WEB_BLOCK_SIZE:get()620local b = file:read(chunk)621sock:set_blocking(true)622if b and #b > 0 then623local sent = sock:send(b, #b)624if sent == -1 then625run = nil626self.remove()627return628end629if sent < #b then630file:seek(offset+sent)631end632offset = offset + sent633end634if not b or #b < chunk then635-- EOF636DEBUG(string.format("%u: sent file", idx))637run = nil638self.remove()639return640end641end642643--[[644load whole file as a string645--]]646function self.load_file()647local chunk = WEB_BLOCK_SIZE:get()648local ret = ""649while true do650local b = file:read(chunk)651if not b or #b == 0 then652break653end654ret = ret .. b655end656return ret657end658659--[[660evaluate some lua code and return as a string661--]]662function self.evaluate(code)663local eval_code = "function eval_func()\n" .. code .. "\nend\n"664local f, errloc, err = load(eval_code, "eval_func", "t", _ENV)665if not f then666DEBUG(string.format("load failed: err=%s errloc=%s", err, errloc))667return nil668end669local success, err2 = pcall(f)670if not success then671DEBUG(string.format("pcall failed: err=%s", err2))672return nil673end674local ok, s2 = pcall(eval_func)675eval_func = nil676if ok then677return s2678end679return nil680end681682--[[683process a file as a lua CGI684--]]685function self.send_cgi()686sock:set_blocking(true)687local contents = self.load_file()688local s = self.evaluate(contents)689if s then690self.sendstring(s)691end692self.remove()693end694695--[[696send file content with server side processsing697files ending in .shtml can have embedded lua lika this:698<?lua return "foo" ?>699<?lstr 2.6+7.2 ?>700701Using 'lstr' a return tostring(yourcode) is added to the code702automatically703--]]704function self.send_processed_file(dynamic_page)705sock:set_blocking(true)706local contents707if dynamic_page then708contents = file709else710contents = self.load_file()711end712while #contents > 0 do713local pat1 = "(.-)[<][?]lua[ \n](.-)[?][>](.*)"714local pat2 = "(.-)[<][?]lstr[ \n](.-)[?][>](.*)"715local p1, p2, p3 = string.match(contents, pat1)716if not p1 then717p1, p2, p3 = string.match(contents, pat2)718if not p1 then719break720end721p2 = "return tostring(" .. p2 .. ")"722end723self.sendstring(p1)724local s2 = self.evaluate(p2)725if s2 then726self.sendstring(s2)727end728contents = p3729end730self.sendstring(contents)731self.remove()732end733734-- return a content type735function self.content_type(path)736if path == "/" then737return MIME_TYPES["html"]738end739local _, ext = string.match(path, '(.*[.])(.*)')740ext = string.lower(ext)741local ret = MIME_TYPES[ext]742if not ret then743return CONTENT_OCTET_STREAM744end745return ret746end747748-- perform a file download749function self.file_download(path)750if startswith(path, "/@") then751path = string.sub(path, 2, #path)752end753DEBUG(string.format("%u: file_download(%s)", idx, path))754file = DYNAMIC_PAGES[path]755dynamic_page = file ~= nil756if not dynamic_page then757file = io.open(path,"rb")758if not file then759DEBUG(string.format("%u: Failed to open '%s'", idx, path))760return false761end762end763local vars = {["Content-Type"]=self.content_type(path)}764local cgi_processing = startswith(path, "/cgi-bin/") and endswith(path, ".lua")765local server_side_processing = endswith(path, ".shtml")766local stat = fs:stat(path)767if not startswith(path, "@") and768not server_side_processing and769not cgi_processing and stat and770not dynamic_page then771local fsize = stat:size()772local mtime = stat:mtime()773vars["Content-Length"]= tostring(fsize)774local modtime = file_timestring_http(mtime)775if modtime then776vars["Last-Modified"] = modtime777end778local if_modified_since = header_vars['If-Modified-Since']779if if_modified_since then780local tsec = file_timestring_http_parse(if_modified_since)781if tsec and tsec >= mtime then782DEBUG(string.format("%u: Not modified: %s %s", idx, modtime, if_modified_since))783self.send_header(304, "Not Modified", vars)784return true785end786end787end788self.send_header(200, "OK", vars)789if server_side_processing or dynamic_page then790DEBUG(string.format("%u: shtml processing %s", idx, path))791run = self.send_processed_file(dynamic_page)792elseif cgi_processing then793DEBUG(string.format("%u: CGI processing %s", idx, path))794run = self.send_cgi795elseif stat and796WEB_SENDFILE_MIN:get() > 0 and797stat:size() >= WEB_SENDFILE_MIN:get() and798sock:sendfile(file) then799return true800else801run = self.send_file802end803return true804end805806function self.not_found()807self.send_header(404, "Not found", {})808end809810function self.moved_permanently(relpath)811if not startswith(relpath, "/") then812relpath = "/" .. relpath813end814local location = string.format("http://%s%s", header_vars['Host'], relpath)815DEBUG(string.format("%u: Redirect -> %s", idx, location))816self.send_header(301, "Moved Permanently", {["Location"]=location})817end818819-- process a single request820function self.process_request()821local h1 = header_lines[1]822if not h1 or #h1 == 0 then823DEBUG(string.format("%u: empty request", idx))824return825end826local cmd = split(header_lines[1], "%S+")827if not cmd or #cmd < 3 then828DEBUG(string.format("bad request: %s", header_lines[1]))829return830end831if cmd[1] ~= "GET" then832DEBUG(string.format("bad op: %s", cmd[1]))833return834end835protocol = cmd[3]836if protocol ~= "HTTP/1.0" and protocol ~= "HTTP/1.1" then837DEBUG(string.format("bad protocol: %s", protocol))838return839end840local path = cmd[2]841DEBUG(string.format("%u: path='%s'", idx, path))842843-- extract header variables844for i = 2,#header_lines do845local key, var = string.match(header_lines[i], '(.*): (.*)')846if key then847header_vars[key] = var848end849end850851if ACTION_PAGES[path] ~= nil then852DEBUG(string.format("Running ACTION %s", path))853local fn = ACTION_PAGES[path]854self.send_header(200, "OK", {["Content-Type"]=CONTENT_TEXT_HTML})855self.sendstring([[856<html>857<head>858<meta http-equiv="refresh" content="2; url=/">859</head>860</html>861]])862fn()863return864end865866if DYNAMIC_PAGES[path] ~= nil then867self.file_download(path)868return869end870871if path == MNT_PREFIX then872path = "/"873end874if startswith(path, MNT_PREFIX2) then875path = string.sub(path,#MNT_PREFIX2,#path)876end877878if isdirectory(path) and879not endswith(path,"/") and880header_vars['Host'] and881not is_hidden_dir(path) then882self.moved_permanently(path .. "/")883return884end885886if path ~= "/" and endswith(path,"/") then887path = string.sub(path, 1, #path-1)888end889890if startswith(path,"/@") then891path = string.sub(path, 2, #path)892end893894-- see if we have an index file895if isdirectory(path) and file_exists(path .. "/index.html") then896DEBUG(string.format("%u: found index.html", idx))897if self.file_download(path .. "/index.html") then898return899end900end901902-- see if it is a directory903if (path == "/" or904DYNAMIC_PAGES[path] == nil) and905(endswith(path,"/") or906isdirectory(path) or907is_hidden_dir(path)) then908self.directory_list(path)909return910end911912-- or a file913if self.file_download(path) then914return915end916self.not_found(path)917end918919-- update the client920function self.update()921if run then922run()923return924end925if not have_header then926if not self.read_header() then927return928end929end930self.process_request()931if not run then932-- nothing more to do933self.remove()934end935end936937function self.remove()938DEBUG(string.format("%u: removing client OFFSET=%u", idx, offset))939if self.closed then940return941end942sock:close()943self.closed = true944end945946-- return the instance947return self948end949950--[[951see if any new clients want to connect952--]]953local function check_new_clients()954while sock_listen:pollin(0) do955local sock = sock_listen:accept()956if not sock then957return958end959-- non-blocking for header read960sock:set_blocking(false)961-- find free client slot962for i = 1, #clients+1 do963if clients[i] == nil then964local idx = i965local client = Client(sock, idx)966DEBUG(string.format("%u: New client", idx))967clients[idx] = client968end969end970end971end972973--[[974check for client activity975--]]976local function check_clients()977for idx,client in ipairs(clients) do978if not client.closed then979client.update()980end981if client.closed then982table.remove(clients,idx)983end984end985end986987local function update()988check_new_clients()989check_clients()990if reboot_counter then991reboot_counter = reboot_counter - 1992if reboot_counter == 0 then993periph:can_printf("Rebooting")994periph:reboot(true)995end996end997return update,5998end9991000return update,100100110021003