httpserver.lua 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. -- httpserver
  2. -- Author: Marcos Kirsch
  3. -- Starts web server in the specified port.
  4. return function (port)
  5. local s = net.createServer(net.TCP, 10) -- 10 seconds client timeout
  6. s:listen(
  7. port,
  8. function (connection)
  9. -- This variable holds the thread (actually a Lua coroutine) used for sending data back to the user.
  10. -- We do it in a separate thread because we need to send in little chunks and wait for the onSent event
  11. -- before we can send more, or we risk overflowing the mcu's buffer.
  12. local connectionThread
  13. local fileInfo
  14. local allowStatic = {GET=true, HEAD=true, POST=false, PUT=false, DELETE=false, TRACE=false, OPTIONS=false, CONNECT=false, PATCH=false}
  15. -- Pretty log function.
  16. local function log(connection, msg, optionalMsg)
  17. local port, ip = connection:getpeer()
  18. if(optionalMsg == nil) then
  19. print(ip .. ":" .. port, msg)
  20. else
  21. print(ip .. ":" .. port, msg, optionalMsg)
  22. end
  23. end
  24. local function startServingStatic(connection, req, args)
  25. fileInfo = dofile("httpserver-static.lc")(connection, req, args)
  26. end
  27. local function startServing(fileServeFunction, connection, req, args)
  28. connectionThread = coroutine.create(function(fileServeFunction, bufferedConnection, req, args)
  29. fileServeFunction(bufferedConnection, req, args)
  30. -- The bufferedConnection may still hold some data that hasn't been sent. Flush it before closing.
  31. if not bufferedConnection:flush() then
  32. log(connection, "closing connection", "no (more) data")
  33. connection:close()
  34. connectionThread = nil
  35. collectgarbage()
  36. end
  37. end)
  38. local BufferedConnectionClass = dofile("httpserver-connection.lc")
  39. local bufferedConnection = BufferedConnectionClass:new(connection)
  40. BufferedConnectionClass = nil
  41. local status, err = coroutine.resume(connectionThread, fileServeFunction, bufferedConnection, req, args)
  42. if not status then
  43. log(connection, "Error: "..err)
  44. log(connection, "closing connection", "error")
  45. connection:close()
  46. connectionThread = nil
  47. collectgarbage()
  48. end
  49. end
  50. local function handleRequest(connection, req, handleError)
  51. collectgarbage()
  52. local method = req.method
  53. local uri = req.uri
  54. local fileServeFunction = nil
  55. if #(uri.file) > 32 then
  56. -- nodemcu-firmware cannot handle long filenames.
  57. uri.args = {code = 400, errorString = "Bad Request", logFunction = log}
  58. fileServeFunction = dofile("httpserver-error.lc")
  59. else
  60. local fileExists = false
  61. if not file.exists(uri.file) then
  62. -- print(uri.file .. " not found, checking gz version...")
  63. -- gzip check
  64. if file.exists(uri.file .. ".gz") then
  65. -- print("gzip variant exists, serving that one")
  66. uri.file = uri.file .. ".gz"
  67. uri.isGzipped = true
  68. fileExists = true
  69. end
  70. else
  71. fileExists = true
  72. end
  73. if not fileExists then
  74. uri.args = {code = 404, errorString = "Not Found", logFunction = log}
  75. fileServeFunction = dofile("httpserver-error.lc")
  76. elseif uri.isScript then
  77. fileServeFunction = dofile(uri.file)
  78. else
  79. if allowStatic[method] then
  80. uri.args = {file = uri.file, ext = uri.ext, isGzipped = uri.isGzipped}
  81. startServingStatic(connection, req, uri.args)
  82. return
  83. else
  84. uri.args = {code = 405, errorString = "Method not supported", logFunction = log}
  85. fileServeFunction = dofile("httpserver-error.lc")
  86. end
  87. end
  88. end
  89. startServing(fileServeFunction, connection, req, uri.args)
  90. end
  91. local function onReceive(connection, payload)
  92. -- collectgarbage()
  93. local conf = dofile("httpserver-conf.lua")
  94. local auth
  95. local user = "Anonymous"
  96. -- as suggest by anyn99 (https://github.com/marcoskirsch/nodemcu-httpserver/issues/36#issuecomment-167442461)
  97. -- Some browsers send the POST data in multiple chunks.
  98. -- Collect data packets until the size of HTTP body meets the Content-Length stated in header
  99. if payload:find("Content%-Length:") or bBodyMissing then
  100. if fullPayload then fullPayload = fullPayload .. payload else fullPayload = payload end
  101. if (tonumber(string.match(fullPayload, "%d+", fullPayload:find("Content%-Length:")+16)) > #fullPayload:sub(fullPayload:find("\r\n\r\n", 1, true)+4, #fullPayload)) then
  102. bBodyMissing = true
  103. return
  104. else
  105. --print("HTTP packet assembled! size: "..#fullPayload)
  106. payload = fullPayload
  107. fullPayload, bBodyMissing = nil
  108. end
  109. end
  110. collectgarbage()
  111. -- parse payload and decide what to serve.
  112. local req = dofile("httpserver-request.lc")(payload)
  113. log(connection, req.method, req.request)
  114. if conf.auth.enabled then
  115. auth = dofile("httpserver-basicauth.lc")
  116. user = auth.authenticate(payload) -- authenticate returns nil on failed auth
  117. end
  118. if user and req.methodIsValid and (req.method == "GET" or req.method == "POST" or req.method == "PUT") then
  119. req.user = user
  120. handleRequest(connection, req, handleError)
  121. else
  122. local args = {}
  123. local fileServeFunction = dofile("httpserver-error.lc")
  124. if not user then
  125. args = {code = 401, errorString = "Not Authorized", headers = {auth.authErrorHeader()}, logFunction = log}
  126. elseif req.methodIsValid then
  127. args = {code = 501, errorString = "Not Implemented", logFunction = log}
  128. else
  129. args = {code = 400, errorString = "Bad Request", logFunction = log}
  130. end
  131. startServing(fileServeFunction, connection, req, args)
  132. end
  133. end
  134. local function onSent(connection, payload)
  135. collectgarbage()
  136. if connectionThread then
  137. local connectionThreadStatus = coroutine.status(connectionThread)
  138. if connectionThreadStatus == "suspended" then
  139. -- Not finished sending file, resume.
  140. local status, err = coroutine.resume(connectionThread)
  141. if not status then
  142. log(connection, "Error: "..err)
  143. log(connection, "closing connection", "error")
  144. connection:close()
  145. connectionThread = nil
  146. collectgarbage()
  147. end
  148. elseif connectionThreadStatus == "dead" then
  149. -- We're done sending file.
  150. log(connection, "closing connection","thread is dead")
  151. connection:close()
  152. connectionThread = nil
  153. collectgarbage()
  154. end
  155. elseif fileInfo then
  156. local fileSize = file.list()[fileInfo.file]
  157. -- Chunks larger than 1024 don't work.
  158. -- https://github.com/nodemcu/nodemcu-firmware/issues/1075
  159. local chunkSize = 512
  160. local fileHandle = file.open(fileInfo.file)
  161. if fileSize > fileInfo.sent then
  162. fileHandle:seek("set", fileInfo.sent)
  163. local chunk = fileHandle:read(chunkSize)
  164. fileHandle:close()
  165. fileHandle = nil
  166. fileInfo.sent = fileInfo.sent + #chunk
  167. connection:send(chunk)
  168. -- print(fileInfo.file .. ": Sent "..#chunk.. " bytes, " .. fileSize - fileInfo.sent .. " to go.")
  169. chunk = nil
  170. else
  171. log(connection, "closing connetion", "Finished sending: "..fileInfo.file)
  172. connection:close()
  173. fileInfo = nil
  174. end
  175. collectgarbage()
  176. end
  177. end
  178. local function onDisconnect(connection, payload)
  179. -- this should rather be a log call, but log is not available here
  180. -- print("disconnected")
  181. if connectionThread then
  182. connectionThread = nil
  183. collectgarbage()
  184. end
  185. if fileInfo then
  186. fileInfo = nil
  187. collectgarbage()
  188. end
  189. end
  190. connection:on("receive", onReceive)
  191. connection:on("sent", onSent)
  192. connection:on("disconnection", onDisconnect)
  193. end
  194. )
  195. return s
  196. end