├── 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 |
10 |
11 |
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Index.html is here!
10 |
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 |
--------------------------------------------------------------------------------