├── example ├── doge.jpg ├── 404.html ├── index.html ├── init.lua └── httpServer.lua ├── README.md └── httpServer.lua /example/doge.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangzexi/NodeMCU-HTTP-Server/HEAD/example/doge.jpg -------------------------------------------------------------------------------- /example/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 404 6 | 7 | 8 |

We found nothing, but a doge.

9 | 10 | 11 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Index Page 6 | 7 | 8 |

Welcome my friend.

9 |

Index.html is here!

10 |

You can access follows to test the server

11 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /example/init.lua: -------------------------------------------------------------------------------- 1 | print('Setting up WIFI...') 2 | wifi.setmode(wifi.STATION) 3 | wifi.sta.config('testwifi', '123456789') 4 | wifi.sta.connect() 5 | 6 | tmr.alarm(1, 1000, tmr.ALARM_AUTO, function() 7 | if wifi.sta.getip() == nil then 8 | print('Waiting for IP ...') 9 | else 10 | print('IP is ' .. wifi.sta.getip()) 11 | tmr.stop(1) 12 | end 13 | end) 14 | 15 | -- Serving static files 16 | dofile('httpServer.lua') 17 | httpServer:listen(80) 18 | 19 | -- Custom API 20 | -- Get text/html 21 | httpServer:use('/welcome', function(req, res) 22 | res:send('Hello ' .. req.query.name) -- /welcome?name=doge 23 | end) 24 | 25 | -- Get file 26 | httpServer:use('/doge', function(req, res) 27 | res:sendFile('doge.jpg') 28 | end) 29 | 30 | -- Get json 31 | httpServer:use('/json', function(req, res) 32 | res:type('application/json') 33 | res:send('{"doge": "smile"}') 34 | end) 35 | 36 | -- Redirect 37 | httpServer:use('/redirect', function(req, res) 38 | res:redirect('doge.jpg') 39 | end) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NodeMCU-HTTP-Server 2 | 3 | A lightweight HTTP server for NodeMCU. 4 | Inspired by [nodemcu_http_server](https://github.com/borischernov/nodemcu_http_server). 5 | 6 | ## Features 7 | 8 | * Serving static files from NodeMCU file system 9 | * Support for gzipped static files 10 | * Support for GET query string parameter 11 | * Redirects 12 | * Support for index.html, 404.html 13 | * Middleware models 14 | 15 | ## Example 16 | 17 | ``` lua 18 | -- init.lua 19 | print('Setting up WIFI...') 20 | wifi.setmode(wifi.STATION) 21 | wifi.sta.config('MY_SSID', 'MY_PASS') 22 | wifi.sta.connect() 23 | 24 | tmr.alarm(1, 1000, tmr.ALARM_AUTO, function() 25 | if wifi.sta.getip() == nil then 26 | print('Waiting for IP ...') 27 | else 28 | print('IP is ' .. wifi.sta.getip()) 29 | tmr.stop(1) 30 | end 31 | end) 32 | 33 | -- Serving static files 34 | dofile('httpServer.lua') 35 | httpServer:listen(80) 36 | 37 | -- Custom API 38 | -- Get text/html 39 | httpServer:use('/welcome', function(req, res) 40 | res:send('Hello ' .. req.query.name) -- /welcome?name=doge 41 | end) 42 | 43 | -- Get file 44 | httpServer:use('/doge', function(req, res) 45 | res:sendFile('doge.jpg') 46 | end) 47 | 48 | -- Get json 49 | httpServer:use('/json', function(req, res) 50 | res:type('application/json') 51 | res:send('{"doge": "smile"}') 52 | end) 53 | 54 | -- Redirect 55 | httpServer:use('/redirect', function(req, res) 56 | res:redirect('doge.jpg') 57 | end) 58 | ``` 59 | 60 | 61 | 62 | ## Request 63 | 64 | ### req.source 65 | 66 | Contains the raw http header and body. 67 | 68 | ### req.ip 69 | 70 | Contains the remote IP address of the request. 71 | 72 | ### req.method 73 | 74 | Contains a string corresponding to the HTTP method of the request: GET, POST, PUT, and so on. 75 | 76 | ### req.query 77 | 78 | This property is an table containing a property for each query string parameter in the route. If there is no query string, it is the empty table, {}. 79 | 80 | ### req.path 81 | 82 | Contains the path part of the request URL. 83 | 84 | 85 | 86 | ## Response 87 | 88 | ### res:redirect(url [, status]) 89 | 90 | Redirects to the URL derived from the specified path, with specified status, a positive integer that corresponds to an HTTP status code . If not specified, status defaults to 302. 91 | 92 | ### res:type(type) 93 | 94 | Sets the Content-Type HTTP header. 95 | 96 | ### res:status(status) 97 | 98 | Sets the HTTP status for the response. 99 | 100 | ### res:send(body) 101 | 102 | Transfers the body, then close. 103 | 104 | If not specified status code using **res:status()**, status defaults to 200. 105 | 106 | If not specified Content-Type using **res:type()**, the Content-Type response HTTP header field will be 'text/html'. 107 | 108 | ### res:sendFile(filename) 109 | 110 | Transfers the file, then close. 111 | 112 | If the file doesn't exist, transfers 404.html.gz/404.html/'404 Not Found'. 113 | 114 | If not specified Content-Type using **res:type()**, the Content-Type response HTTP header field will base on the filename’s extension. 115 | 116 | If not specified status code using **res:status()**, status defaults to 200. 117 | 118 | ### res:close() 119 | 120 | Ends the response process. 121 | 122 | 123 | 124 | ## Middleware models 125 | request -> parseHeader -> **user middleware** -> staticFile 126 | 127 | The order of middleware loading is important: middleware functions that are loaded first are also executed first. 128 | 129 | If one of the user middleware functions has **nil/false** return value, the request process will be stopped. 130 | 131 | ### httpServer:use(url, callback) 132 | 133 | The first parameter url is a lua pattern, i.e. **'\\foo.*'** can match '\foo.html', '\foo\bar.jpg'... 134 | 135 | 136 | 137 | ## Serving static files 138 | 139 | If any request not processed by user middleware functions, serving static file will be attempted. 140 | 141 | ### Static files matching rule example 142 | 143 | | url | 1st try | 2nd try | 3th try | 4th try | 144 | | :----------- | :------------- | :---------- | :---------- | :------- | 145 | | / | index.html.gz | index.html | 404.html.gz | 404.html | 146 | | /foo.jpg | foo.jpg.gz | foo.jpg | 404.html.gz | 404.html | 147 | | /foo/bar.css | foo_bar.css.gz | foo_bar.css | 404.html.gz | 404.html | 148 | | /foo | foo.gz | foo | 404.html.gz | 404.html | 149 | 150 | Slashes in request path are converted to underscores to get appropriate file name. 151 | 152 | Static files may be gzipped to reduce used filesystem space. For a static file gzipped version is searched for first. 153 | 154 | Server tries to guess content types for the most common file extensions used, see function **guessType()** for details. 155 | 156 | 157 | 158 | ## License 159 | 160 | MIT -------------------------------------------------------------------------------- /example/httpServer.lua: -------------------------------------------------------------------------------- 1 | -------------------- 2 | -- helper 3 | -------------------- 4 | function urlDecode(url) 5 | return url:gsub('%%(%x%x)', function(x) 6 | return string.char(tonumber(x, 16)) 7 | end) 8 | end 9 | 10 | function guessType(filename) 11 | local types = { 12 | ['.css'] = 'text/css', 13 | ['.js'] = 'application/javascript', 14 | ['.html'] = 'text/html', 15 | ['.png'] = 'image/png', 16 | ['.jpg'] = 'image/jpeg' 17 | } 18 | for ext, type in pairs(types) do 19 | if string.sub(filename, -string.len(ext)) == ext 20 | or string.sub(filename, -string.len(ext .. '.gz')) == ext .. '.gz' then 21 | return type 22 | end 23 | end 24 | return 'text/plain' 25 | end 26 | 27 | -------------------- 28 | -- Response 29 | -------------------- 30 | Res = { 31 | _skt = nil, 32 | _type = nil, 33 | _status = nil, 34 | _redirectUrl = nil, 35 | } 36 | 37 | function Res:new(skt) 38 | local o = {} 39 | setmetatable(o, self) 40 | self.__index = self 41 | o._skt = skt 42 | return o 43 | end 44 | 45 | function Res:redirect(url, status) 46 | status = status or 302 47 | 48 | self:status(status) 49 | self._redirectUrl = url 50 | self:send(status) 51 | end 52 | 53 | function Res:type(type) 54 | self._type = type 55 | end 56 | 57 | function Res:status(status) 58 | self._status = status 59 | end 60 | 61 | function Res:send(body) 62 | self._status = self._status or 200 63 | self._type = self._type or 'text/html' 64 | 65 | local buf = 'HTTP/1.1 ' .. self._status .. '\r\n' 66 | .. 'Content-Type: ' .. self._type .. '\r\n' 67 | .. 'Content-Length:' .. string.len(body) .. '\r\n' 68 | if self._redirectUrl ~= nil then 69 | buf = buf .. 'Location: ' .. self._redirectUrl .. '\r\n' 70 | end 71 | buf = buf .. '\r\n' .. body 72 | 73 | local function doSend() 74 | if buf == '' then 75 | self:close() 76 | else 77 | self._skt:send(string.sub(buf, 1, 512)) 78 | buf = string.sub(buf, 513) 79 | end 80 | end 81 | self._skt:on('sent', doSend) 82 | 83 | doSend() 84 | end 85 | 86 | function Res:sendFile(filename) 87 | if file.exists(filename .. '.gz') then 88 | filename = filename .. '.gz' 89 | elseif not file.exists(filename) then 90 | self:status(404) 91 | if filename == '404.html' then 92 | self:send(404) 93 | else 94 | self:sendFile('404.html') 95 | end 96 | return 97 | end 98 | 99 | self._status = self._status or 200 100 | local header = 'HTTP/1.1 ' .. self._status .. '\r\n' 101 | 102 | self._type = self._type or guessType(filename) 103 | 104 | header = header .. 'Content-Type: ' .. self._type .. '\r\n' 105 | if string.sub(filename, -3) == '.gz' then 106 | header = header .. 'Content-Encoding: gzip\r\n' 107 | end 108 | header = header .. '\r\n' 109 | 110 | print('* Sending ', filename) 111 | local pos = 0 112 | local function doSend() 113 | file.open(filename, 'r') 114 | if file.seek('set', pos) == nil then 115 | self:close() 116 | print('* Finished ', filename) 117 | else 118 | local buf = file.read(512) 119 | pos = pos + 512 120 | self._skt:send(buf) 121 | end 122 | file.close() 123 | end 124 | self._skt:on('sent', doSend) 125 | 126 | self._skt:send(header) 127 | end 128 | 129 | function Res:close() 130 | self._skt:on('sent', function() end) -- release closures context 131 | self._skt:on('receive', function() end) 132 | self._skt:close() 133 | self._skt = nil 134 | end 135 | 136 | -------------------- 137 | -- Middleware 138 | -------------------- 139 | function parseHeader(req, res) 140 | local _, _, method, path, vars = string.find(req.source, '([A-Z]+) (.+)?(.+) HTTP') 141 | if method == nil then 142 | _, _, method, path = string.find(req.source, '([A-Z]+) (.+) HTTP') 143 | end 144 | local _GET = {} 145 | if vars ~= nil then 146 | vars = urlDecode(vars) 147 | for k, v in string.gmatch(vars, '([^&]+)=([^&]*)&*') do 148 | _GET[k] = v 149 | end 150 | end 151 | 152 | req.method = method 153 | req.query = _GET 154 | req.path = path 155 | 156 | return true 157 | end 158 | 159 | function staticFile(req, res) 160 | local filename = '' 161 | if req.path == '/' then 162 | filename = 'index.html' 163 | else 164 | filename = string.gsub(string.sub(req.path, 2), '/', '_') 165 | end 166 | 167 | res:sendFile(filename) 168 | end 169 | 170 | -------------------- 171 | -- HttpServer 172 | -------------------- 173 | httpServer = { 174 | _srv = nil, 175 | _mids = {{ 176 | url = '.*', 177 | cb = parseHeader 178 | }, { 179 | url = '.*', 180 | cb = staticFile 181 | }} 182 | } 183 | 184 | function httpServer:use(url, cb) 185 | table.insert(self._mids, #self._mids, { 186 | url = url, 187 | cb = cb 188 | }) 189 | end 190 | 191 | function httpServer:close() 192 | self._srv:close() 193 | self._srv = nil 194 | end 195 | 196 | function httpServer:listen(port) 197 | self._srv = net.createServer(net.TCP) 198 | self._srv:listen(port, function(conn) 199 | conn:on('receive', function(skt, msg) 200 | local req = { source = msg, path = '', ip = skt:getpeer() } 201 | local res = Res:new(skt) 202 | 203 | for i = 1, #self._mids do 204 | if string.find(req.path, '^' .. self._mids[i].url .. '$') 205 | and not self._mids[i].cb(req, res) then 206 | break 207 | end 208 | end 209 | 210 | collectgarbage() 211 | end) 212 | end) 213 | end -------------------------------------------------------------------------------- /httpServer.lua: -------------------------------------------------------------------------------- 1 | -------------------- 2 | -- helper 3 | -------------------- 4 | function urlDecode(url) 5 | return url:gsub('%%(%x%x)', function(x) 6 | return string.char(tonumber(x, 16)) 7 | end) 8 | end 9 | 10 | function guessType(filename) 11 | local types = { 12 | ['.css'] = 'text/css', 13 | ['.js'] = 'application/javascript', 14 | ['.html'] = 'text/html', 15 | ['.png'] = 'image/png', 16 | ['.jpg'] = 'image/jpeg' 17 | } 18 | for ext, type in pairs(types) do 19 | if string.sub(filename, -string.len(ext)) == ext 20 | or string.sub(filename, -string.len(ext .. '.gz')) == ext .. '.gz' then 21 | return type 22 | end 23 | end 24 | return 'text/plain' 25 | end 26 | 27 | -------------------- 28 | -- Response 29 | -------------------- 30 | Res = { 31 | _skt = nil, 32 | _type = nil, 33 | _status = nil, 34 | _redirectUrl = nil, 35 | } 36 | 37 | function Res:new(skt) 38 | local o = {} 39 | setmetatable(o, self) 40 | self.__index = self 41 | o._skt = skt 42 | return o 43 | end 44 | 45 | function Res:redirect(url, status) 46 | status = status or 302 47 | 48 | self:status(status) 49 | self._redirectUrl = url 50 | self:send(status) 51 | end 52 | 53 | function Res:type(type) 54 | self._type = type 55 | end 56 | 57 | function Res:status(status) 58 | self._status = status 59 | end 60 | 61 | function Res:send(body) 62 | self._status = self._status or 200 63 | self._type = self._type or 'text/html' 64 | 65 | local buf = 'HTTP/1.1 ' .. self._status .. '\r\n' 66 | .. 'Content-Type: ' .. self._type .. '\r\n' 67 | .. 'Content-Length:' .. string.len(body) .. '\r\n' 68 | if self._redirectUrl ~= nil then 69 | buf = buf .. 'Location: ' .. self._redirectUrl .. '\r\n' 70 | end 71 | buf = buf .. '\r\n' .. body 72 | 73 | local function doSend() 74 | if buf == '' then 75 | self:close() 76 | else 77 | self._skt:send(string.sub(buf, 1, 512)) 78 | buf = string.sub(buf, 513) 79 | end 80 | end 81 | self._skt:on('sent', doSend) 82 | 83 | doSend() 84 | end 85 | 86 | function Res:sendFile(filename) 87 | if file.exists(filename .. '.gz') then 88 | filename = filename .. '.gz' 89 | elseif not file.exists(filename) then 90 | self:status(404) 91 | if filename == '404.html' then 92 | self:send(404) 93 | else 94 | self:sendFile('404.html') 95 | end 96 | return 97 | end 98 | 99 | self._status = self._status or 200 100 | local header = 'HTTP/1.1 ' .. self._status .. '\r\n' 101 | 102 | self._type = self._type or guessType(filename) 103 | 104 | header = header .. 'Content-Type: ' .. self._type .. '\r\n' 105 | if string.sub(filename, -3) == '.gz' then 106 | header = header .. 'Content-Encoding: gzip\r\n' 107 | end 108 | header = header .. '\r\n' 109 | 110 | print('* Sending ', filename) 111 | local pos = 0 112 | local function doSend() 113 | file.open(filename, 'r') 114 | if file.seek('set', pos) == nil then 115 | self:close() 116 | print('* Finished ', filename) 117 | else 118 | local buf = file.read(512) 119 | pos = pos + 512 120 | self._skt:send(buf) 121 | end 122 | file.close() 123 | end 124 | self._skt:on('sent', doSend) 125 | 126 | self._skt:send(header) 127 | end 128 | 129 | function Res:close() 130 | self._skt:on('sent', function() end) -- release closures context 131 | self._skt:on('receive', function() end) 132 | self._skt:close() 133 | self._skt = nil 134 | end 135 | 136 | -------------------- 137 | -- Middleware 138 | -------------------- 139 | function parseHeader(req, res) 140 | local _, _, method, path, vars = string.find(req.source, '([A-Z]+) (.+)?(.*) HTTP') 141 | if method == nil then 142 | _, _, method, path = string.find(req.source, '([A-Z]+) (.+) HTTP') 143 | end 144 | local _GET = {} 145 | if (vars ~= nil and vars ~= '') then 146 | vars = urlDecode(vars) 147 | for k, v in string.gmatch(vars, '([^&]+)=([^&]*)&*') do 148 | _GET[k] = v 149 | end 150 | end 151 | 152 | req.method = method 153 | req.query = _GET 154 | req.path = path 155 | 156 | return true 157 | end 158 | 159 | function staticFile(req, res) 160 | local filename = '' 161 | if req.path == '/' then 162 | filename = 'index.html' 163 | else 164 | filename = string.gsub(string.sub(req.path, 2), '/', '_') 165 | end 166 | 167 | res:sendFile(filename) 168 | end 169 | 170 | -------------------- 171 | -- HttpServer 172 | -------------------- 173 | httpServer = { 174 | _srv = nil, 175 | _mids = {{ 176 | url = '.*', 177 | cb = parseHeader 178 | }, { 179 | url = '.*', 180 | cb = staticFile 181 | }} 182 | } 183 | 184 | function httpServer:use(url, cb) 185 | table.insert(self._mids, #self._mids, { 186 | url = url, 187 | cb = cb 188 | }) 189 | end 190 | 191 | function httpServer:close() 192 | self._srv:close() 193 | self._srv = nil 194 | end 195 | 196 | function httpServer:listen(port) 197 | self._srv = net.createServer(net.TCP) 198 | self._srv:listen(port, function(conn) 199 | conn:on('receive', function(skt, msg) 200 | local req = { source = msg, path = '', ip = skt:getpeer() } 201 | local res = Res:new(skt) 202 | 203 | for i = 1, #self._mids do 204 | if string.find(req.path, '^' .. self._mids[i].url .. '$') 205 | and not self._mids[i].cb(req, res) then 206 | break 207 | end 208 | end 209 | 210 | collectgarbage() 211 | end) 212 | end) 213 | end 214 | --------------------------------------------------------------------------------