From b875d5b80d91f3bb4896f30d292b597bfcbd6eb5 Mon Sep 17 00:00:00 2001 From: Ryan Voots Date: Sun, 1 May 2016 15:18:34 -0700 Subject: [PATCH] Add http server code. --- nodemcu/http/apple-touch-icon.png | Bin 0 -> 4183 bytes nodemcu/http/args.lua | 26 ++++++ nodemcu/http/file_list.lua | 30 ++++++ nodemcu/http/garage_door_opener.css | 134 +++++++++++++++++++++++++++ nodemcu/http/garage_door_opener.html | 84 +++++++++++++++++ nodemcu/http/garage_door_opener.lua | 32 +++++++ nodemcu/http/index.html | 3 +- nodemcu/http/node_info.lua | 26 ++++++ nodemcu/http/post.lua | 33 +++++++ nodemcu/http/underconstruction.gif | Bin 0 -> 2689 bytes nodemcu/http/zipped.html.gz | Bin 0 -> 363 bytes nodemcu/httpserver-b64decode.lua | 65 +++++++++++++ nodemcu/httpserver-basicauth.lua | 29 ++++++ nodemcu/httpserver-conf.lua | 15 +++ nodemcu/httpserver-error.lua | 19 ++++ nodemcu/httpserver-header.lua | 35 +++++++ nodemcu/httpserver-request.lua | 115 +++++++++++++++++++++++ nodemcu/httpserver-static.lua | 31 +++++++ nodemcu/httpserver.lua | 123 ++++++++++++++++++++++++ nodemcu/inithttp.lua | 78 ++++++++++++++++ 20 files changed, 876 insertions(+), 2 deletions(-) create mode 100644 nodemcu/http/apple-touch-icon.png create mode 100644 nodemcu/http/args.lua create mode 100644 nodemcu/http/file_list.lua create mode 100644 nodemcu/http/garage_door_opener.css create mode 100644 nodemcu/http/garage_door_opener.html create mode 100644 nodemcu/http/garage_door_opener.lua create mode 100644 nodemcu/http/node_info.lua create mode 100644 nodemcu/http/post.lua create mode 100644 nodemcu/http/underconstruction.gif create mode 100644 nodemcu/http/zipped.html.gz create mode 100755 nodemcu/httpserver-b64decode.lua create mode 100644 nodemcu/httpserver-basicauth.lua create mode 100644 nodemcu/httpserver-conf.lua create mode 100644 nodemcu/httpserver-error.lua create mode 100644 nodemcu/httpserver-header.lua create mode 100644 nodemcu/httpserver-request.lua create mode 100644 nodemcu/httpserver-static.lua create mode 100644 nodemcu/httpserver.lua create mode 100644 nodemcu/inithttp.lua diff --git a/nodemcu/http/apple-touch-icon.png b/nodemcu/http/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..de91784669ad68839fa56547bb5df8a687335e03 GIT binary patch literal 4183 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 literal 0 HcmV?d00001 diff --git a/nodemcu/http/args.lua b/nodemcu/http/args.lua new file mode 100644 index 0000000..f7a2790 --- /dev/null +++ b/nodemcu/http/args.lua @@ -0,0 +1,26 @@ +return function (connection, req, args) + connection:send("HTTP/1.0 200 OK\r\nContent-Type: text/html\r\nCache-Control: private, no-store\r\n\r\n") + connection:send('Arguments') + connection:send('') + connection:send('

Arguments

') + + local form = [===[ +
+ First name:

+ Last name:

+ MaleFemale
+ +
+ ]===] + + connection:send(form) + + connection:send('

Received the following values:

') + connection:send("
    \n") + for name, value in pairs(args) do + connection:send('
  • ' .. name .. ': ' .. tostring(value) .. "
  • \n") + end + + connection:send("
\n") + connection:send('') +end diff --git a/nodemcu/http/file_list.lua b/nodemcu/http/file_list.lua new file mode 100644 index 0000000..9e92a23 --- /dev/null +++ b/nodemcu/http/file_list.lua @@ -0,0 +1,30 @@ +return function (connection, req, args) + connection:send("HTTP/1.0 200 OK\r\nContent-Type: text/html\r\nCache-Control: private, no-store\r\n\r\n") + connection:send('Server File Listing') + connection:send('') + coroutine.yield() + connection:send('

Server File Listing

') + + local remaining, used, total=file.fsinfo() + connection:send("Total size: " .. total .. " bytes
\n") + connection:send("In Use: " .. used .. " bytes
\n") + connection:send("Free: " .. remaining .. " bytes
\n") + + connection:send("

\n") + connection:send("Files:
\n") + connection:send("

    \n") + + for name, size in pairs(file.list()) do + + local isHttpFile = string.match(name, "(http/)") ~= nil + if isHttpFile then + local url = string.match(name, ".*/(.*)") + connection:send('
  • ' .. url .. " (" .. size .. " bytes)
  • \n") + -- this list could be very long, so we'll yield in order to avoid overflowing the send buffer. + coroutine.yield() + end + end + connection:send("
\n") + connection:send("

\n") + connection:send('') +end diff --git a/nodemcu/http/garage_door_opener.css b/nodemcu/http/garage_door_opener.css new file mode 100644 index 0000000..bcaa154 --- /dev/null +++ b/nodemcu/http/garage_door_opener.css @@ -0,0 +1,134 @@ +html, body { + height:100%; + margin: 0; + overflow: hidden; +} + +body { + + text-align: center; + background-color: black; + min-height: 100%; + color: black; +} + + +#remote { + background-color: #666; + width: 90%; + border-radius: 30px; + margin: 5% 5% 0; + height: 90%; + padding: 0; +} + +#spacer { + clear: both; + border-top: 1px solid rgba(0, 0, 0, 0.5); + -moz-box-shadow: 1px 1px 1px; + box-shadow: 1px 1px 1px; + margin-right: 30px; + margin-left: 30px; +} + + +.button { + display: inline-block; + width: 43%; + margin: 20px 0 30px; + padding: 40px 0; + border-style: none; + color: rgba(192, 192, 192, 0.5); + text-decoration: none; + border-radius: 20px; + text-shadow: 0 0 1px rgba(0, 0, 0, 0.5); + font-size: 130px; + font-weight: bold; + background-color: #CCC; + -moz-box-shadow: 0 10px rgba(0, 0, 0, 0.25); + box-shadow: 0 10px rgba(0, 0, 0, 0.25); + position: relative; +} + + +.button-1 { + float: left; + margin-left: 5%; +} + +.button-2 { + float: right; + margin-right: 5%; +} + + + +.button span { + + +} + + + +.button:hover span { + + +} + + +.button:active, .button:focus { + + +} + + + +.button:active span { + + +} + + +#label { + font-family: "Lucida Grande", Lucida, Verdana, sans-serif; + background-color: rgba(0, 0, 0, 0.1); + width: 12px; + height: 12px; + display: block; + margin: 20px auto; + -webkit-border-radius: 20px; + -moz-border-radius: 20px; + border-radius: 20px; + text-indent: -99999px; + top: 20px; + position: relative; +} + +#label.start { + +} + +#label.initalizing { + +} + +#label.connection { + background-color: orange; +} + +#label.received { + background-color: orange; +} + +#label.processing { + background-color: orange; +} + +#label.ok { + background-color: green; +} + +#label.bad { + background-color: red; +} + diff --git a/nodemcu/http/garage_door_opener.html b/nodemcu/http/garage_door_opener.html new file mode 100644 index 0000000..cc67ca2 --- /dev/null +++ b/nodemcu/http/garage_door_opener.html @@ -0,0 +1,84 @@ + + + + + + + Garage Remote + + + + + + + diff --git a/nodemcu/http/garage_door_opener.lua b/nodemcu/http/garage_door_opener.lua new file mode 100644 index 0000000..ca67617 --- /dev/null +++ b/nodemcu/http/garage_door_opener.lua @@ -0,0 +1,32 @@ +-- garage_door_opener.lua +-- Part of nodemcu-httpserver, example. +-- Author: Marcos Kirsch + +local function pushTheButton(connection, pin) + + -- push the button! + -- Note that the relays connected to the garage door opener are wired + -- to close when the GPIO pin is low. This way they don't activate when + -- the chip is reset and the GPIO pins are in input mode. + gpio.write(pin, gpio.LOW) + gpio.mode(pin, gpio.OUTPUT) + gpio.write(pin, gpio.LOW) + tmr.delay(300000) -- in microseconds + gpio.write(pin, gpio.HIGH) + gpio.mode(pin, gpio.INPUT) + + -- Send back JSON response. + connection:send("HTTP/1.0 200 OK\r\nContent-Type: application/json\r\nCache-Control: private, no-store\r\n\r\n") + connection:send('{"error":0, "message":"OK"}') + +end + +return function (connection, req, args) + print('Garage door button was pressed!', args.door) + if args.door == "1" then pushTheButton(connection, 1) -- GPIO1 + elseif args.door == "2" then pushTheButton(connection, 2) -- GPIO2 + else + connection:send("HTTP/1.0 400 OK\r\nContent-Type: application/json\r\nCache-Control: private, no-store\r\n\r\n") + connection:send('{"error":-1, "message":"Bad door"}') + end +end diff --git a/nodemcu/http/index.html b/nodemcu/http/index.html index 321de45..0db6ace 100644 --- a/nodemcu/http/index.html +++ b/nodemcu/http/index.html @@ -22,12 +22,11 @@
  • Index: This page (static)
  • Zipped: A compressed file (static)
  • Arguments: Parses arguments passed in the URL and prints them. (Lua)
  • -
  • Post: A form that uses POST method, should error. (static)
  • +
  • Post: A form that uses POST method. Displays different content based on HTTP method. (Lua)
  • Garage door opener: Control GPIO lines via the server. (Lua)
  • NodeMCU info: Shows some basic NodeMCU(Lua)
  • List all server files: Displays a list of all the server files. (Lua)
  • Foo: A file that doesn't exist. Should error (404 error)
  • -
  • DaBomb: A working POST example
  • diff --git a/nodemcu/http/node_info.lua b/nodemcu/http/node_info.lua new file mode 100644 index 0000000..86e55eb --- /dev/null +++ b/nodemcu/http/node_info.lua @@ -0,0 +1,26 @@ +local function sendHeader(connection) + connection:send("HTTP/1.0 200 OK\r\nContent-Type: text/html\r\nCache-Control: private, no-store\r\n\r\n") + +end + +local function sendAttr(connection, attr, val) + connection:send("
  • ".. attr .. ": " .. val .. "
  • \n") +end + +return function (connection, req, args) + collectgarbage() + sendHeader(connection) + connection:send('A Lua script sample

    Node info

      ') + majorVer, minorVer, devVer, chipid, flashid, flashsize, flashmode, flashspeed = node.info(); + sendAttr(connection, "NodeMCU version" , majorVer.."."..minorVer.."."..devVer) + sendAttr(connection, "chipid" , chipid) + sendAttr(connection, "flashid" , flashid) + sendAttr(connection, "flashsize" , flashsize) + sendAttr(connection, "flashmode" , flashmode) + sendAttr(connection, "flashspeed" , flashspeed) + sendAttr(connection, "node.heap()" , node.heap()) + sendAttr(connection, 'Memory in use (KB)' , collectgarbage("count")) + sendAttr(connection, 'IP address' , wifi.sta.getip()) + sendAttr(connection, 'MAC address' , wifi.sta.getmac()) + connection:send('
    ') +end diff --git a/nodemcu/http/post.lua b/nodemcu/http/post.lua new file mode 100644 index 0000000..d1ac03d --- /dev/null +++ b/nodemcu/http/post.lua @@ -0,0 +1,33 @@ +return function (connection, req, args) + connection:send("HTTP/1.0 200 OK\r\nContent-Type: text/html\r\nCache-Control: private, no-store\r\n\r\n") + connection:send('Arguments') + connection:send('') + connection:send('

    Arguments

    ') + + local form = [===[ +
    + First name:

    + Last name:

    + MaleFemale
    + +
    + ]===] + + if req.method == "GET" then + connection:send(form) + elseif req.method == "POST" then + local rd = req.getRequestData() + -- connection:send(cjson.encode(rd)) + connection:send('

    Received the following values:

    ') + connection:send("
      \n") + for name, value in pairs(rd) do + connection:send('
    • ' .. name .. ': ' .. tostring(value) .. "
    • \n") + end + + connection:send("
    \n") + else + connection:send("NOT IMPLEMENTED") + end + + connection:send('') +end diff --git a/nodemcu/http/underconstruction.gif b/nodemcu/http/underconstruction.gif new file mode 100644 index 0000000000000000000000000000000000000000..7d61c1a7cbdc45fa67511ff0b035b9545e1e0ed5 GIT binary patch literal 2689 zcmeHGX;YI|6uzJqk+y;+C>4;fqhQf17D1eXglMUVkOYFJLWGMfqeTOVOI>h@sHkjm z6;MzqP#J`R;Na5Mg(9yMtYTD9qjkY4ky@)l(cAN;ANm9O#p(3UL{i1CMGj50pZ97Q|} zXcEd1!V%07#FK(qOgT(Aj5&;eBV6SWT=m!#@5Lg2k zkOr183OryjU;{K@A>KK@4K*B02;z+deXtfPj0r~IE64>F&?E>G1U`b(U;yxfaWERx zf|cM0(1hDy2XF$xpb%IC7?6gWKnr-lV!#Gy#7D$WLmO&12oeJ$K_9Gz3M0e^Xo#O@ zVMvtE(-0C*%HTU6ufa;5qrn|MZG&K*vOxnMzJV66z_1CR;qL$`fq9@wk5CUn%?MQ? zq(LYhAsIq}2-zcK0W|(aTd&vm^z<}0H&<0vX*8Pj^mLg_78n?4Z*Om5!DEY)JZ*~SFeCoc+li6{5|EmZ92>XN&T|T$GR;nvVW&uoTV)rT5T6>SMxue?rnWHZ z4nKHDo(PS6gN#O6&pmP2A5E%@U6^vB^!)Y@W#)x*H;OLo9B=xWWB;?efjh0{%;?tE z=C@?ohbI$3pY?u%oZXIJIy0Nq=5Hu*$Jb9vx2%HhiASGpC|GZ_(xhPPrdAWXU_pRM+1v=HW%ojShsrH8 z|I~@^hpnqDRIc&fr!GmybEoo{?+W|~LGS>JOin|1Q1-9;L=NpfpppS0YhZ$`;1`sq(?YfTcT zRC@2cd%9$**|pP(TC>)Qi8a~B^Y8=h9)-)Jp%wjo_0sv7^S^oR5#L>D>T0g{u2ema z-2K?5xxb>j-FNrxkf=gY>)o8XL#tX6eUf$VWeLwtTE9*j_^tF@QeUC?`~Lp>rr(4x zflt`rS22>#3=2i$1SvaxL;jVjFMjVjov0yibM*%jyCrWL;ts4AyggzQIZicPw9z_M ztI${}^AB$l^mSwfjPe>EmF4dCV)Pf2AEl0g5Ce7U59{(piLOx>RZ%@&52Y(s3M(DA z^zS$pOP)H#Yx9DqJ#s7vMDmFJcuV9z_#+5Furp!(HdTmRi&4pDFMaAC7dm9zL zYqKXPYmA?U-PX(n)ULBNY z#!c;5UK=AWGG@aI#pwe(UVgE`xI|*BdetW=_5X9BMk@K%W?R(i5kb>+OaJgFwOCRTt4{3@=|lt$8ut-2S}xwEzMkc$ub(ry?aE2f zB5OYJ&@e%9w%m!1TOM&=ur#vB>*nVpqn!l#9Kl4hJ&^ od!<=3&$?9<|B;{Zd4X%4O`(-T*FcS*R|9`jtg?H=w7jSmbp#T5? literal 0 HcmV?d00001 diff --git a/nodemcu/http/zipped.html.gz b/nodemcu/http/zipped.html.gz new file mode 100644 index 0000000000000000000000000000000000000000..8bd5b55f21428c60a89f9ef209bc4932951b1d88 GIT binary patch literal 363 zcmV-x0hIn9iwFoS1~gRw1A1w2aAjmJXmo9C098^uPs1<}-jVnZ*DQcU8FWOR9ZQ;Zd~u%}&yHk%L> zEmr_243dCGiV%@@Sx?<^o6T3GNMpHo#s`cMTj-R=B2O^0pJyG(x1l>p-Q!q$rQ$B+_IW;YFip0H(rk3TRLdpp{XCZBi<7Kdoq94R0d-48LNT JkaQjb004U`rW61G literal 0 HcmV?d00001 diff --git a/nodemcu/httpserver-b64decode.lua b/nodemcu/httpserver-b64decode.lua new file mode 100755 index 0000000..e18bea6 --- /dev/null +++ b/nodemcu/httpserver-b64decode.lua @@ -0,0 +1,65 @@ +#!/usr/local/bin/lua +-- httpserver-b64decode.lua +-- Part of nodemcu-httpserver, contains b64 decoding used for HTTP Basic Authentication. +-- Based on http://lua-users.org/wiki/BaseSixtyFour by Alex Kloss +-- compatible with lua 5.1 +-- http://www.it-rfc.de +-- Author: Marcos Kirsch + +-- bitshift functions (<<, >> equivalent) +-- shift left +local function lsh(value,shift) + return (value*(2^shift)) % 256 +end + +-- shift right +local function rsh(value,shift) + -- Lua builds with no floating point don't define math. + if math then return math.floor(value/2^shift) % 256 end + return (value/2^shift) % 256 +end + +-- return single bit (for OR) +local function bit(x,b) + return (x % 2^b - x % 2^(b-1) > 0) +end + +-- logic OR for number values +local function lor(x,y) + result = 0 + for p=1,8 do result = result + (((bit(x,p) or bit(y,p)) == true) and 2^(p-1) or 0) end + return result +end + +-- Character decoding table +local function toBase64Byte(char) + ascii = string.byte(char, 1) + if ascii >= string.byte('A', 1) and ascii <= string.byte('Z', 1) then return ascii - string.byte('A', 1) + elseif ascii >= string.byte('a', 1) and ascii <= string.byte('z', 1) then return ascii - string.byte('a', 1) + 26 + elseif ascii >= string.byte('0', 1) and ascii <= string.byte('9', 1) then return ascii + 4 + elseif ascii == string.byte('-', 1) then return 62 + elseif ascii == string.byte('_', 1) then return 63 + elseif ascii == string.byte('=', 1) then return nil + else return nil, "ERROR! Char is invalid for Base64 encoding: "..char end +end + + +-- decode base64 input to string +return function(data) + local chars = {} + local result="" + for dpos=0,string.len(data)-1,4 do + for char=1,4 do chars[char] = toBase64Byte((string.sub(data,(dpos+char),(dpos+char)) or "=")) end + result = string.format( + '%s%s%s%s', + result, + string.char(lor(lsh(chars[1],2), rsh(chars[2],4))), + (chars[3] ~= nil) and string.char(lor(lsh(chars[2],4), + rsh(chars[3],2))) or "", + (chars[4] ~= nil) and string.char(lor(lsh(chars[3],6) % 192, + (chars[4]))) or "" + ) + end + return result +end + diff --git a/nodemcu/httpserver-basicauth.lua b/nodemcu/httpserver-basicauth.lua new file mode 100644 index 0000000..8ff7d25 --- /dev/null +++ b/nodemcu/httpserver-basicauth.lua @@ -0,0 +1,29 @@ +-- httpserver-basicauth.lua +-- Part of nodemcu-httpserver, authenticates a user using http basic auth. +-- Author: Sam Dieck + +basicAuth = {} + +function basicAuth.authenticate(header) + conf = dofile("httpserver-conf.lc") + -- Parse basic auth http header. + -- Returns the username if header contains valid credentials, + -- nil otherwise. + local credentials_enc = header:match("Authorization: Basic ([A-Za-z0-9+/=]+)") + if not credentials_enc then + return nil + end + local credentials = dofile("httpserver-b64decode.lc")(credentials_enc) + local user, pwd = credentials:match("^(.*):(.*)$") + if user ~= conf.auth.user or pwd ~= conf.auth.password then + return nil + end + print("httpserver-basicauth: User \"" .. user .. "\" authenticated.") + return user +end + +function basicAuth.authErrorHeader() + return "WWW-Authenticate: Basic realm=\"" .. conf.auth.realm .. "\"" +end + +return basicAuth diff --git a/nodemcu/httpserver-conf.lua b/nodemcu/httpserver-conf.lua new file mode 100644 index 0000000..854e557 --- /dev/null +++ b/nodemcu/httpserver-conf.lua @@ -0,0 +1,15 @@ +-- httpserver-conf.lua +-- Part of nodemcu-httpserver, contains static configuration for httpserver. +-- Author: Sam Dieck + +local conf = {} + +-- Basic Authentication Conf +local auth = {} +auth.enabled = false +auth.realm = "nodemcu-httpserver" -- displayed in the login dialog users get +auth.user = "user" +auth.password = "password" -- PLEASE change this +conf.auth = auth + +return conf diff --git a/nodemcu/httpserver-error.lua b/nodemcu/httpserver-error.lua new file mode 100644 index 0000000..b4dba56 --- /dev/null +++ b/nodemcu/httpserver-error.lua @@ -0,0 +1,19 @@ +-- httpserver-error.lua +-- Part of nodemcu-httpserver, handles sending error pages to client. +-- Author: Marcos Kirsch + +return function (connection, req, args) + + local function sendHeader(connection, code, errorString, extraHeaders, mimeType) + connection:send("HTTP/1.0 " .. code .. " " .. errorString .. "\r\nServer: nodemcu-httpserver\r\nContent-Type: " .. mimeType .. "\r\n") + for i, header in ipairs(extraHeaders) do + connection:send(header .. "\r\n") + end + connection:send("connection: close\r\n\r\n") + end + + print("Error " .. args.code .. ": " .. args.errorString) + args.headers = args.headers or {} + sendHeader(connection, args.code, args.errorString, args.headers, "text/html") + connection:send("" .. args.code .. " - " .. args.errorString .. "

    " .. args.code .. " - " .. args.errorString .. "

    \r\n") +end diff --git a/nodemcu/httpserver-header.lua b/nodemcu/httpserver-header.lua new file mode 100644 index 0000000..5f01ef7 --- /dev/null +++ b/nodemcu/httpserver-header.lua @@ -0,0 +1,35 @@ +-- httpserver-header.lua +-- Part of nodemcu-httpserver, knows how to send an HTTP header. +-- Author: Marcos Kirsch + +return function (connection, code, extension) + + local function getHTTPStatusString(code) + local codez = {[200]="OK", [400]="Bad Request", [404]="Not Found",} + local myResult = codez[code] + -- enforce returning valid http codes all the way throughout? + if myResult then return myResult else return "Not Implemented" end + end + + local function getMimeType(ext) + local gzip = false + -- A few MIME types. Keep list short. If you need something that is missing, let's add it. + local mt = {css = "text/css", gif = "image/gif", html = "text/html", ico = "image/x-icon", jpeg = "image/jpeg", jpg = "image/jpeg", js = "application/javascript", json = "application/json", png = "image/png", xml = "text/xml"} + -- add comressed flag if file ends with gz + if ext:find("%.gz$") then + ext = ext:sub(1, -4) + gzip = true + end + if mt[ext] then contentType = mt[ext] else contentType = "text/plain" end + return {contentType = contentType, gzip = gzip} + end + + local mimeType = getMimeType(extension) + + connection:send("HTTP/1.0 " .. code .. " " .. getHTTPStatusString(code) .. "\r\nServer: nodemcu-httpserver\r\nContent-Type: " .. mimeType["contentType"] .. "\r\n") + if mimeType["gzip"] then + connection:send("Content-Encoding: gzip\r\n") + end + connection:send("Connection: close\r\n\r\n") +end + diff --git a/nodemcu/httpserver-request.lua b/nodemcu/httpserver-request.lua new file mode 100644 index 0000000..121dbf8 --- /dev/null +++ b/nodemcu/httpserver-request.lua @@ -0,0 +1,115 @@ +-- httpserver-request +-- Part of nodemcu-httpserver, parses incoming client requests. +-- Author: Marcos Kirsch + +local function validateMethod(method) + local httpMethods = {GET=true, HEAD=true, POST=true, PUT=true, DELETE=true, TRACE=true, OPTIONS=true, CONNECT=true, PATCH=true} + -- default for non-existent attributes returns nil, which evaluates to false + return httpMethods[method] +end + +local function uriToFilename(uri) + return "http/" .. string.sub(uri, 2, -1) +end + +local function hex_to_char(x) + return string.char(tonumber(x, 16)) +end + +local function uri_decode(input) + return input:gsub("%+", " "):gsub("%%(%x%x)", hex_to_char) +end + +local function parseArgs(args) + local r = {}; i=1 + if args == nil or args == "" then return r end + for arg in string.gmatch(args, "([^&]+)") do + local name, value = string.match(arg, "(.*)=(.*)") + if name ~= nil then r[name] = uri_decode(value) end + i = i + 1 + end + return r +end + +local function parseFormData(body) + local data = {} + print("Parsing Form Data") + for kv in body.gmatch(body, "%s*&?([^=]+=[^&]+)") do + local key, value = string.match(kv, "(.*)=(.*)") + + print("Parsed: " .. key .. " => " .. value) + data[key] = uri_decode(value) + end + + return data +end + +local function getRequestData(payload) + local requestData + return function () + print("Getting Request Data") + if requestData then + return requestData + else + local mimeType = string.match(payload, "Content%-Type: (%S+)\r\n") + local body_start = payload:find("\r\n\r\n", 1, true) + local body = payload:sub(body_start, #payload) + payload = nil + collectgarbage() + + -- print("mimeType = [" .. mimeType .. "]") + + if mimeType == "application/json" then + print("JSON: " .. body) + requestData = cjson.decode(body) + elseif mimeType == "application/x-www-form-urlencoded" then + requestData = parseFormData(body) + else + requestData = {} + end + + return requestData + end + end +end + +local function parseUri(uri) + local r = {} + local filename + local ext + local fullExt = {} + + if uri == nil then return r end + if uri == "/" then uri = "/index.html" end + questionMarkPos, b, c, d, e, f = uri:find("?") + if questionMarkPos == nil then + r.file = uri:sub(1, questionMarkPos) + r.args = {} + else + r.file = uri:sub(1, questionMarkPos - 1) + r.args = parseArgs(uri:sub(questionMarkPos+1, #uri)) + end + filename = r.file + while filename:match("%.") do + filename,ext = filename:match("(.+)%.(.+)") + table.insert(fullExt,1,ext) + end + r.ext = table.concat(fullExt,".") + r.isScript = r.ext == "lua" or r.ext == "lc" + r.file = uriToFilename(r.file) + return r +end + +-- Parses the client's request. Returns a dictionary containing pretty much everything +-- the server needs to know about the uri. +return function (request) + local e = request:find("\r\n", 1, true) + if not e then return nil end + local line = request:sub(1, e - 1) + local r = {} + _, i, r.method, r.request = line:find("^([A-Z]+) (.-) HTTP/[1-9]+.[0-9]+$") + r.methodIsValid = validateMethod(r.method) + r.uri = parseUri(r.request) + r.getRequestData = getRequestData(request) + return r +end diff --git a/nodemcu/httpserver-static.lua b/nodemcu/httpserver-static.lua new file mode 100644 index 0000000..93ecc2a --- /dev/null +++ b/nodemcu/httpserver-static.lua @@ -0,0 +1,31 @@ +-- httpserver-static.lua +-- Part of nodemcu-httpserver, handles sending static files to client. +-- Author: Marcos Kirsch + +return function (connection, req, args) + dofile("httpserver-header.lc")(connection, 200, args.ext) + --print("Begin sending:", args.file) + -- Send file in little chunks + local continue = true + local bytesSent = 0 + while continue do + collectgarbage() + -- NodeMCU file API lets you open 1 file at a time. + -- So we need to open, seek, close each time in order + -- to support multiple simultaneous clients. + file.open(args.file) + file.seek("set", bytesSent) + local chunk = file.read(256) + file.close() + if chunk == nil then + continue = false + else + coroutine.yield() + connection:send(chunk) + bytesSent = bytesSent + #chunk + chunk = nil + --print("Sent" .. args.file, bytesSent) + end + end + --print("Finished sending:", args.file) +end diff --git a/nodemcu/httpserver.lua b/nodemcu/httpserver.lua new file mode 100644 index 0000000..2b0376a --- /dev/null +++ b/nodemcu/httpserver.lua @@ -0,0 +1,123 @@ +-- httpserver +-- Author: Marcos Kirsch + +-- Starts web server in the specified port. +return function (port) + + local s = net.createServer(net.TCP, 10) -- 10 seconds client timeout + s:listen( + port, + function (connection) + + -- This variable holds the thread used for sending data back to the user. + -- We do it in a separate thread because we need to yield when sending lots + -- of data in order to avoid overflowing the mcu's buffer. + local connectionThread + + local allowStatic = {GET=true, HEAD=true, POST=false, PUT=false, DELETE=false, TRACE=false, OPTIONS=false, CONNECT=false, PATCH=false} + + local function onRequest(connection, req) + collectgarbage() + local method = req.method + local uri = req.uri + local fileServeFunction = nil + + print("Method: " .. method); + + if #(uri.file) > 32 then + -- nodemcu-firmware cannot handle long filenames. + uri.args = {code = 400, errorString = "Bad Request"} + fileServeFunction = dofile("httpserver-error.lc") + else + local fileExists = file.open(uri.file, "r") + file.close() + + if not fileExists then + -- gzip check + fileExists = file.open(uri.file .. ".gz", "r") + file.close() + + if fileExists then + print("gzip variant exists, serving that one") + uri.file = uri.file .. ".gz" + uri.ext = uri.ext .. ".gz" + end + end + + if not fileExists then + uri.args = {code = 404, errorString = "Not Found"} + fileServeFunction = dofile("httpserver-error.lc") + elseif uri.isScript then + fileServeFunction = dofile(uri.file) + else + if allowStatic[method] then + uri.args = {file = uri.file, ext = uri.ext} + fileServeFunction = dofile("httpserver-static.lc") + else + uri.args = {code = 405, errorString = "Method not supported"} + fileServeFunction = dofile("httpserver-error.lc") + end + end + end + connectionThread = coroutine.create(fileServeFunction) + coroutine.resume(connectionThread, connection, req, uri.args) + end + + local function onReceive(connection, payload) + collectgarbage() + local conf = dofile("httpserver-conf.lc") + local auth + local user = "Anonymous" + + -- parse payload and decide what to serve. + local req = dofile("httpserver-request.lc")(payload) + print("Requested URI: " .. req.request) + if conf.auth.enabled then + auth = dofile("httpserver-basicauth.lc") + user = auth.authenticate(payload) -- authenticate returns nil on failed auth + end + + if user and req.methodIsValid and (req.method == "GET" or req.method == "POST" or req.method == "PUT") then + onRequest(connection, req) + else + local args = {} + local fileServeFunction = dofile("httpserver-error.lc") + if not user then + args = {code = 401, errorString = "Not Authorized", headers = {auth.authErrorHeader()}} + elseif req.methodIsValid then + args = {code = 501, errorString = "Not Implemented"} + else + args = {code = 400, errorString = "Bad Request"} + end + connectionThread = coroutine.create(fileServeFunction) + coroutine.resume(connectionThread, connection, req, args) + end + end + + local function onSent(connection, payload) + collectgarbage() + if connectionThread then + local connectionThreadStatus = coroutine.status(connectionThread) + if connectionThreadStatus == "suspended" then + -- Not finished sending file, resume. + coroutine.resume(connectionThread) + elseif connectionThreadStatus == "dead" then + -- We're done sending file. + connection:close() + connectionThread = nil + end + end + end + + connection:on("receive", onReceive) + connection:on("sent", onSent) + + end + ) + -- false and nil evaluate as false + local ip = wifi.sta.getip() + if not ip then ip = wifi.ap.getip() end + print("nodemcu-httpserver running at http://" .. ip .. ":" .. port) + return s + +end diff --git a/nodemcu/inithttp.lua b/nodemcu/inithttp.lua new file mode 100644 index 0000000..60e5faf --- /dev/null +++ b/nodemcu/inithttp.lua @@ -0,0 +1,78 @@ +-- Begin WiFi configuration + +local wifiConfig = {} + +-- wifi.STATION -- station: join a WiFi network +-- wifi.AP -- access point: create a WiFi network +-- wifi.wifi.STATIONAP -- both station and access point +wifiConfig.mode = wifi.STATIONAP -- both station and access point + +wifiConfig.accessPointConfig = {} +wifiConfig.accessPointConfig.ssid = "ESP-"..node.chipid() -- Name of the SSID you want to create +wifiConfig.accessPointConfig.pwd = "ESP-"..node.chipid() -- WiFi password - at least 8 characters + +wifiConfig.stationPointConfig = {} +wifiConfig.stationPointConfig.ssid = "Scalar24" -- Name of the WiFi network you want to join +wifiConfig.stationPointConfig.pwd = "Fb274Gh@12G1" -- Password for the WiFi network + +-- Tell the chip to connect to the access point + +wifi.setmode(wifiConfig.mode) +print('set (mode='..wifi.getmode()..')') +print('MAC: ',wifi.sta.getmac()) +print('chip: ',node.chipid()) +print('heap: ',node.heap()) + +wifi.ap.config(wifiConfig.accessPointConfig) +wifi.sta.config(wifiConfig.stationPointConfig.ssid, wifiConfig.stationPointConfig.pwd) +wifiConfig = nil +collectgarbage() + +-- End WiFi configuration + +-- Compile server code and remove original .lua files. +-- This only happens the first time afer the .lua files are uploaded. + +local compileAndRemoveIfNeeded = function(f) + if file.open(f) then + file.close() + print('Compiling:', f) + node.compile(f) + file.remove(f) + collectgarbage() + end +end + +local serverFiles = {'httpserver.lua', 'httpserver-basicauth.lua', 'httpserver-conf.lua', 'httpserver-b64decode.lua', 'httpserver-request.lua', 'httpserver-static.lua', 'httpserver-header.lua', 'httpserver-error.lua'} +for i, f in ipairs(serverFiles) do compileAndRemoveIfNeeded(f) end + +compileAndRemoveIfNeeded = nil +serverFiles = nil +collectgarbage() + +-- Connect to the WiFi access point. +-- Once the device is connected, you may start the HTTP server. + +local joinCounter = 0 +local joinMaxAttempts = 5 +tmr.alarm(0, 3000, 1, function() + local ip = wifi.sta.getip() + if ip == nil and joinCounter < joinMaxAttempts then + print('Connecting to WiFi Access Point ...') + joinCounter = joinCounter +1 + else + if joinCounter == joinMaxAttempts then + print('Failed to connect to WiFi Access Point.') + else + print('IP: ',ip) + -- Uncomment to automatically start the server in port 80 + dofile("httpserver.lc")(80) + end + tmr.stop(0) + joinCounter = nil + joinMaxAttempts = nil + collectgarbage() + end + +end) +