From 77920f4a9414970360eb892c3a4dd56386aa96eb Mon Sep 17 00:00:00 2001 From: Marcos Kirsch Date: Sat, 28 Feb 2015 16:39:06 -0600 Subject: [PATCH] Big refactoring: server now uses a separate thread that yields and then resumes on 'sent'. This allows us to serve large files. Moved serving of error pages and serving of static files into separate scripts httpserver-error.lua and httpserver-static.lua --- httpserver-error.lua | 21 +++++++++++ httpserver-static.lua | 28 +++++++++++++++ httpserver.lua | 84 +++++++++++++++++++------------------------ 3 files changed, 86 insertions(+), 47 deletions(-) create mode 100644 httpserver-error.lua create mode 100644 httpserver-static.lua diff --git a/httpserver-error.lua b/httpserver-error.lua new file mode 100644 index 0000000..405d45b --- /dev/null +++ b/httpserver-error.lua @@ -0,0 +1,21 @@ +-- httpserver-error.lua +-- Part of nodemcu-httpserver, handles sending error pages to client. +-- Author: Marcos Kirsch + +local function getHTTPStatusString(code) + if code == 404 then return "Not Found" end + if code == 400 then return "Bad Request" end + if code == 501 then return "Not Implemented" end + return "Unknown HTTP status" +end + +local function sendHeader(connection, code, codeString, mimeType) + connection:send("HTTP/1.0 " .. code .. " " .. codeString .. "\r\nServer: nodemcu-httpserver\r\nContent-Type: " .. mimeType .. "\r\nConnection: close\r\n\r\n") +end + +return function (connection, args) + errorString = getHTTPStatusString(args.code) + print("Error: " .. args.code .. ": " .. errorString) + sendHeader(connection, args.code, errorString, "text/html") + connection:send("" .. args.code .. " - " .. errorString .. "

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

\r\n") +end diff --git a/httpserver-static.lua b/httpserver-static.lua new file mode 100644 index 0000000..c1a9579 --- /dev/null +++ b/httpserver-static.lua @@ -0,0 +1,28 @@ +-- httpserver-static.lua +-- Part of nodemcu-httpserver, handles sending static files to client. +-- Author: Marcos Kirsch + +local function getMimeType(ext) + -- 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",png = "image/png"} + if mt[ext] then return mt[ext] else return "text/plain" end +end + +local function sendHeader(connection, code, codeString, mimeType) + connection:send("HTTP/1.0 " .. code .. " " .. codeString .. "\r\nServer: nodemcu-httpserver\r\nContent-Type: " .. mimeType .. "\r\nConnection: close\r\n\r\n") +end + +return function (connection, args) + print("Serving:", args.file) + sendHeader(connection, 200, "OK", getMimeType(args.ext)) + file.open(args.file) + -- Send file in little chunks + while true do + local chunk = file.read(512) + if chunk == nil then break end + coroutine.yield() + connection:send(chunk) + end + print("Finished sending file.") + file.close() +end diff --git a/httpserver.lua b/httpserver.lua index 92dca0d..dc71481 100644 --- a/httpserver.lua +++ b/httpserver.lua @@ -25,12 +25,6 @@ local function uriToFilename(uri) return "http/" .. string.sub(uri, 2, -1) end -local function onError(connection, errorCode, errorString) - print(errorCode .. ": " .. errorString) - connection:send("HTTP/1.0 " .. errorCode .. " " .. errorString .. "\r\nContent-Type: text/html\r\nConnection: close\r\n\r\n") - connection:send("" .. errorCode .. " - " .. errorString .. "

" .. errorCode .. " - " .. errorString .. "

\r\n") -end - local function parseArgs(args) local r = {}; i=1 if args == nil or args == "" then return r end @@ -60,45 +54,29 @@ local function parseUri(uri) return r end -local function getMimeType(ext) - -- A few MIME types. No need to go crazy in this list. If you need something that is missing, let's add it. - local mt = {} - mt.css = "text/css" - mt.gif = "image/gif" - mt.html = "text/html" - mt.ico = "image/x-icon" - mt.jpeg = "image/jpeg" - mt.jpg = "image/jpeg" - mt.js = "application/javascript" - mt.png = "image/png" - if mt[ext] then return mt[ext] end - -- default to text. - return "text/plain" -end +-- 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 function onGet(connection, uri) local uri = parseUri(uri) local fileExists = file.open(uri.file, "r") + file.close() + local fileServeFunction = nil if not fileExists then - onError(connection, 404, "Not Found") + uri.args['code'] = 404 + fileServeFunction = dofile("httpserver-error.lc") + elseif uri.isScript then + collectgarbage() + fileServeFunction = dofile(uri.file) else - if uri.isScript then - file.close() - collectgarbage() - dofile(uri.file)(connection, uri.args) - else - -- Use HTTP/1.0 to ensure client closes connection. - connection:send("HTTP/1.0 200 OK\r\nContent-Type: " .. getMimeType(uri.ext) .. "\r\Cache-Control: private, no-store\r\n\r\n") - -- Send file in little 128-byte chunks - while true do - local chunk = file.read(128) - if chunk == nil then break end - connection:send(chunk) - end - file.close() - end + uri.args['file'] = uri.file + uri.args['ext'] = uri.ext + fileServeFunction = dofile("httpserver-static.lc") end - collectgarbage() + connectionThread = coroutine.create(fileServeFunction) + coroutine.resume(connectionThread, connection, uri.args) end local function onReceive(connection, payload) @@ -107,22 +85,34 @@ local function onReceive(connection, payload) local req = parseRequest(payload) print("Requested URI: " .. req.uri) req.method = validateMethod(req.method) - if req.method == nil then onError(connection, 400, "Bad Request") - elseif req.method == "GET" then onGet(connection, req.uri) - else onError(connection, 501, "Not Implemented") end - connection:close() + if req.method == "GET" then onGet(connection, req.uri) + elseif req.method == nil then dofile("httpserver-static.lc")(conection, {code=400}) + else dofile("httpserver-static.lc")(conection, {code=501}) end +end + +local function onSent(connection, payload) + if coroutine.status(connectionThread) == "dead" then + -- We're done sending file. + connection:close() + connectionThread = nil + elseif coroutine.status(connectionThread) == "suspended" then + -- Not finished sending file, resume. + coroutine.resume(connectionThread) + else + print ("Fatal error! I did not expect to hit this codepath") + connection:close() + end end local function handleRequest(connection) connection:on("receive", onReceive) + connection:on("sent", onSent) end -- Starts web server in the specified port. -function httpserver.start(port, clientTimeoutInSeconds) - s = net.createServer(net.TCP, clientTimeoutInSeconds) +return function (port) + local s = net.createServer(net.TCP, 10) -- 10 seconds client timeout s:listen(port, handleRequest) - print("nodemcu-httpserver running at " .. wifi.sta.getip() .. ":" .. port) + print("nodemcu-httpserver running at http://" .. wifi.sta.getip() .. ":" .. port) return s end - -