Browse Source

Implement support for more arbitrary HTTP methods. Allows GET PUT and POST. With minor changes needed to support others

Ryan Voots 8 years ago
parent
commit
ef340bc82a
12 changed files with 128 additions and 37 deletions
  1. 4 3
      README.md
  2. 1 1
      http/args.lua
  3. 1 1
      http/file_list.lua
  4. 1 1
      http/garage_door_opener.lua
  5. 1 1
      http/index.html
  6. 1 1
      http/node_info.lua
  7. 0 7
      http/post.html
  8. 33 0
      http/post.lua
  9. 1 1
      httpserver-error.lua
  10. 52 1
      httpserver-request.lua
  11. 1 1
      httpserver-static.lua
  12. 32 19
      httpserver.lua

+ 4 - 3
README.md

@@ -3,13 +3,14 @@ A (very) simple web server written in Lua for the ESP8266 running the NodeMCU fi
 
 ## Features
 
-* GET
+* GET, POST, PUT and minor changes to support other methods
 * Multiple MIME types
 * Error pages (404 and others)
 * Server-side execution of Lua scripts
-* Query string argument parsing
+* Query string argument parsing with decoding of arguments
 * Serving .gz compressed files
 * HTTP Basic Authentication
+* Decoding of request bodies in both application/x-www-form-urlencoded and application/json (if cjson is available)
 
 ## How to use
 
@@ -130,7 +131,7 @@ A (very) simple web server written in Lua for the ESP8266 running the NodeMCU fi
 
 ## Not supported
 
-* Other methods: HEAD, POST, PUT, DELETE, TRACE, OPTIONS, CONNECT, PATCH
+* ~~Other methods: HEAD, POST, PUT, DELETE, TRACE, OPTIONS, CONNECT, PATCH~~
 * Encryption
 * Multiple users (HTTP Basic Authentication)
 * Only protect certain directories (HTTP Basic Authentication)

+ 1 - 1
http/args.lua

@@ -1,4 +1,4 @@
-return function (connection, args)
+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('<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><title>Arguments</title></head>')
    connection:send('<body>')

+ 1 - 1
http/file_list.lua

@@ -1,4 +1,4 @@
-return function (connection, args)
+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('<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><title>Server File Listing</title></head>')
    connection:send('<body>')

+ 1 - 1
http/garage_door_opener.lua

@@ -21,7 +21,7 @@ local function pushTheButton(connection, pin)
 
 end
 
-return function (connection, args)
+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

+ 1 - 1
http/index.html

@@ -22,7 +22,7 @@
       <li><a href="index.html">Index</a>: This page (static)</li>
       <li><a href="zipped.html.gz">Zipped</a>: A compressed file (static)</li>
       <li><a href="args.lua">Arguments</a>: Parses arguments passed in the URL and prints them. (Lua)</li>
-      <li><a href="post.html">Post</a>: A form that uses POST method, should error. (static)</li>
+      <li><a href="post.lua">Post</a>: A form that uses POST method.  Displays different content based on HTTP method. (Lua)</li>
       <li><a href="garage_door_opener.html">Garage door opener</a>: Control GPIO lines via the server. (Lua)</li>
       <li><a href="node_info.lua">NodeMCU info</a>: Shows some basic NodeMCU(Lua)</li>
       <li><a href="file_list.lua">List all server files</a>: Displays a list of all the server files. (Lua)</li>

+ 1 - 1
http/node_info.lua

@@ -7,7 +7,7 @@ local function sendAttr(connection, attr, val)
    connection:send("<li><b>".. attr .. ":</b> " .. val .. "<br></li>\n")
 end
 
-return function (connection, args)
+return function (connection, req, args)
    collectgarbage()
    sendHeader(connection)
    connection:send('<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><title>A Lua script sample</title></head><body><h1>Node info</h1><ul>')

+ 0 - 7
http/post.html

@@ -1,7 +0,0 @@
-<!DOCTYPE html>
-<html lang="en"><head><meta charset="utf-8"><title>Post</title></head><body>
-<h1>Post</h1>
-This form uses POST method which is not supported by nodemcu-httpserver.<br>
-You should get an error when you press submit.
-<form method="POST"><input type="submit" value="Submit"></form>
-</body></html>

+ 33 - 0
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('<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><title>Arguments</title></head>')
+   connection:send('<body>')
+   connection:send('<h1>Arguments</h1>')
+
+   local form = [===[
+   <form method="POST">
+      First name:<br><input type="text" name="firstName"><br>
+      Last name:<br><input type="text" name="lastName"><br>
+      <input type="radio" name="sex" value="male" checked>Male<input type="radio" name="sex" value="female">Female<br>
+      <input type="submit" value="Submit">
+   </form>
+   ]===]
+   
+   if req.method == "GET" then
+      connection:send(form)
+   elseif req.method == "POST" then
+     local rd = req.getRequestData()
+   --   connection:send(cjson.encode(rd))
+      connection:send('<h2>Received the following values:</h2>')
+      connection:send("<ul>\n")
+      for name, value in pairs(rd) do
+          connection:send('<li><b>' .. name .. ':</b> ' .. tostring(value) .. "<br></li>\n")
+      end
+
+      connection:send("</ul>\n")
+   else
+      connection:send("NOT IMPLEMENTED")
+   end
+   
+   connection:send('</body></html>')
+end

+ 1 - 1
httpserver-error.lua

@@ -2,7 +2,7 @@
 -- Part of nodemcu-httpserver, handles sending error pages to client.
 -- Author: Marcos Kirsch
 
-return function (connection, args)
+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")

+ 52 - 1
httpserver-request.lua

@@ -12,17 +12,67 @@ 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] = value end
+      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
@@ -60,5 +110,6 @@ return function (request)
    _, 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

+ 1 - 1
httpserver-static.lua

@@ -2,7 +2,7 @@
 -- Part of nodemcu-httpserver, handles sending static files to client.
 -- Author: Marcos Kirsch
 
-return function (connection, args)
+return function (connection, req, args)
    dofile("httpserver-header.lc")(connection, 200, args.ext)
    --print("Begin sending:", args.file)
    -- Send file in little chunks

+ 32 - 19
httpserver.lua

@@ -13,42 +13,54 @@ return function (port)
          -- 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 onGet(connection, uri)
+         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 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"
+                 if fileExists then
+                    print("gzip variant exists, serving that one")
+                    uri.file = uri.file .. ".gz"
+                    uri.ext = uri.ext .. ".gz"
+                 end
                end
-            end
-			   
-            if not fileExists then
+
+               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
-                  uri.args = {file = uri.file, ext = uri.ext}
-                  fileServeFunction = dofile("httpserver-static.lc")
+                  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, uri.args)
+            coroutine.resume(connectionThread, connection, req, uri.args)
          end
 
          local function onReceive(connection, payload)
@@ -64,8 +76,9 @@ return function (port)
                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" then
-               onGet(connection, req.uri)
+
+            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")
@@ -77,7 +90,7 @@ return function (port)
                   args = {code = 400, errorString = "Bad Request"}
                end
                connectionThread = coroutine.create(fileServeFunction)
-               coroutine.resume(connectionThread, connection, args)
+               coroutine.resume(connectionThread, connection, req, args)
             end
          end