-- https://github.com/wangzexi/NodeMCU-HTTP-Server -------------------- -- helper -------------------- function urlDecode(url) return url:gsub('%%(%x%x)', function(x) return string.char(tonumber(x, 16)) end) end function guessType(filename) local types = { ['.css'] = 'text/css', ['.js'] = 'application/javascript', ['.html'] = 'text/html', ['.png'] = 'image/png', ['.jpg'] = 'image/jpeg' } for ext, type in pairs(types) do if string.sub(filename, -string.len(ext)) == ext or string.sub(filename, -string.len(ext .. '.gz')) == ext .. '.gz' then return type end end return 'text/plain' end -------------------- -- Response -------------------- Res = { _skt = nil, _type = nil, _status = nil, _redirectUrl = nil, } function Res:new(skt) local o = {} setmetatable(o, self) self.__index = self o._skt = skt return o end function Res:redirect(url, status) status = status or 302 self:status(status) self._redirectUrl = url self:send(status) end function Res:type(type) self._type = type end function Res:status(status) self._status = status end function Res:send(body) self._status = self._status or 200 self._type = self._type or 'text/html' local buf = 'HTTP/1.1 ' .. self._status .. '\r\n' .. 'Content-Type: ' .. self._type .. '\r\n' .. 'Content-Length:' .. string.len(body) .. '\r\n' if self._redirectUrl ~= nil then buf = buf .. 'Location: ' .. self._redirectUrl .. '\r\n' end buf = buf .. '\r\n' .. body local function doSend() if buf == '' then self:close() else self._skt:send(string.sub(buf, 1, 512)) buf = string.sub(buf, 513) end end self._skt:on('sent', doSend) doSend() end function Res:sendFile(filename) if file.exists(filename .. '.gz') then filename = filename .. '.gz' elseif not file.exists(filename) then self:status(404) if filename == '404.html' then self:send(404) else self:sendFile('404.html') end return end self._status = self._status or 200 local header = 'HTTP/1.1 ' .. self._status .. '\r\n' self._type = self._type or guessType(filename) header = header .. 'Content-Type: ' .. self._type .. '\r\n' if string.sub(filename, -3) == '.gz' then header = header .. 'Content-Encoding: gzip\r\n' end header = header .. '\r\n' print('* Sending ', filename) local pos = 0 local function doSend() file.open(filename, 'r') if file.seek('set', pos) == nil then self:close() print('* Finished ', filename) else local buf = file.read(512) pos = pos + 512 self._skt:send(buf) end file.close() end self._skt:on('sent', doSend) self._skt:send(header) end function Res:close() self._skt:on('sent', function() end) -- release closures context self._skt:on('receive', function() end) self._skt:close() self._skt = nil end -------------------- -- Middleware -------------------- function parseHeader(req, res) local _, _, method, path, vars = string.find(req.source, '([A-Z]+) (.+)?(.+) HTTP') if method == nil then _, _, method, path = string.find(req.source, '([A-Z]+) (.+) HTTP') end local _GET = {} if vars ~= nil then vars = urlDecode(vars) for k, v in string.gmatch(vars, '([^&]+)=([^&]*)&*') do _GET[k] = v end end req.method = method req.query = _GET req.path = path return true end function staticFile(req, res) local filename = '' if req.path == '/' then filename = 'index.html' else filename = string.gsub(string.sub(req.path, 2), '/', '_') end res:sendFile(filename) end -------------------- -- HttpServer -------------------- httpServer = { _srv = nil, _mids = {{ url = '.*', cb = parseHeader }, { url = '.*', cb = staticFile }} } function httpServer:use(url, cb) table.insert(self._mids, #self._mids, { url = url, cb = cb }) end function httpServer:close() self._srv:close() self._srv = nil end function httpServer:listen(port) self._srv = net.createServer(net.TCP) self._srv:listen(port, function(conn) conn:on('receive', function(skt, msg) local req = { source = msg, path = '', ip = skt:getpeer() } local res = Res:new(skt) for i = 1, #self._mids do if string.find(req.path, '^' .. self._mids[i].url .. '$') and not self._mids[i].cb(req, res) then break end end collectgarbage() end) end) end