|
1 |
|
2 local socket = require "socket" |
|
3 local server = require "net.server" |
|
4 local url_parse = require "socket.url".parse; |
|
5 |
|
6 local connlisteners_start = require "net.connlisteners".start; |
|
7 local connlisteners_get = require "net.connlisteners".get; |
|
8 local listener; |
|
9 |
|
10 local t_insert, t_concat = table.insert, table.concat; |
|
11 local s_match, s_gmatch = string.match, string.gmatch; |
|
12 local tonumber, tostring, pairs = tonumber, tostring, pairs; |
|
13 |
|
14 local urlcodes = setmetatable({}, { __index = function (t, k) t[k] = char(tonumber("0x"..k)); return t[k]; end }); |
|
15 local urlencode = function (s) return s and (s:gsub("%W", function (c) return string.format("%%%x", c:byte()); end)); end |
|
16 |
|
17 local log = require "util.logger".init("httpserver"); |
|
18 |
|
19 local http_servers = {}; |
|
20 |
|
21 module "httpserver" |
|
22 |
|
23 local default_handler; |
|
24 |
|
25 local function expectbody(reqt) |
|
26 return reqt.method == "POST"; |
|
27 end |
|
28 |
|
29 local function send_response(request, response) |
|
30 -- Write status line |
|
31 local resp; |
|
32 if response.body then |
|
33 log("debug", "Sending response to %s: %s", request.id, response.body); |
|
34 resp = { "HTTP/1.0 ", response.status or "200 OK", "\r\n"}; |
|
35 local h = response.headers; |
|
36 if h then |
|
37 for k, v in pairs(h) do |
|
38 t_insert(resp, k); |
|
39 t_insert(resp, ": "); |
|
40 t_insert(resp, v); |
|
41 t_insert(resp, "\r\n"); |
|
42 end |
|
43 end |
|
44 if response.body and not (h and h["Content-Length"]) then |
|
45 t_insert(resp, "Content-Length: "); |
|
46 t_insert(resp, #response.body); |
|
47 t_insert(resp, "\r\n"); |
|
48 end |
|
49 t_insert(resp, "\r\n"); |
|
50 |
|
51 if response.body and request.method ~= "HEAD" then |
|
52 t_insert(resp, response.body); |
|
53 end |
|
54 else |
|
55 -- Response we have is just a string (the body) |
|
56 log("debug", "Sending response to %s: %s", request.id, response); |
|
57 |
|
58 resp = { "HTTP/1.0 200 OK\r\n" }; |
|
59 t_insert(resp, "Connection: close\r\n"); |
|
60 t_insert(resp, "Content-Length: "); |
|
61 t_insert(resp, #response); |
|
62 t_insert(resp, "\r\n\r\n"); |
|
63 |
|
64 t_insert(resp, response); |
|
65 end |
|
66 request.write(t_concat(resp)); |
|
67 if not request.stayopen then |
|
68 request:destroy(); |
|
69 end |
|
70 end |
|
71 |
|
72 local function call_callback(request, err) |
|
73 if request.handled then return; end |
|
74 request.handled = true; |
|
75 local callback = request.callback; |
|
76 if not callback and request.path then |
|
77 local path = request.url.path; |
|
78 local base = path:match("^/([^/?]+)"); |
|
79 if not base then |
|
80 base = path:match("^http://[^/?]+/([^/?]+)"); |
|
81 end |
|
82 |
|
83 callback = (request.server and request.server.handlers[base]) or default_handler; |
|
84 if callback == default_handler then |
|
85 log("debug", "Default callback for this request (base: "..tostring(base)..")") |
|
86 end |
|
87 end |
|
88 if callback then |
|
89 if err then |
|
90 log("debug", "Request error: "..err); |
|
91 if not callback(nil, err, request) then |
|
92 destroy_request(request); |
|
93 end |
|
94 return; |
|
95 end |
|
96 |
|
97 local response = callback(request.method, request.body and t_concat(request.body), request); |
|
98 if response then |
|
99 if response == true then |
|
100 -- Keep connection open, we will reply later |
|
101 log("warn", "Request %s left open, on_destroy is %s", request.id, tostring(request.on_destroy)); |
|
102 else |
|
103 -- Assume response |
|
104 send_response(request, response); |
|
105 destroy_request(request); |
|
106 end |
|
107 else |
|
108 log("debug", "Request handler provided no response, destroying request..."); |
|
109 -- No response, close connection |
|
110 destroy_request(request); |
|
111 end |
|
112 end |
|
113 end |
|
114 |
|
115 local function request_reader(request, data, startpos) |
|
116 if not data then |
|
117 if request.body then |
|
118 call_callback(request); |
|
119 else |
|
120 -- Error.. connection was closed prematurely |
|
121 call_callback(request, "connection-closed"); |
|
122 end |
|
123 -- Here we force a destroy... the connection is gone, so we can't reply later |
|
124 destroy_request(request); |
|
125 return; |
|
126 end |
|
127 if request.state == "body" then |
|
128 log("debug", "Reading body...") |
|
129 if not request.body then request.body = {}; request.havebodylength, request.bodylength = 0, tonumber(request.responseheaders["content-length"]); end |
|
130 if startpos then |
|
131 data = data:sub(startpos, -1) |
|
132 end |
|
133 t_insert(request.body, data); |
|
134 if request.bodylength then |
|
135 request.havebodylength = request.havebodylength + #data; |
|
136 if request.havebodylength >= request.bodylength then |
|
137 -- We have the body |
|
138 call_callback(request); |
|
139 end |
|
140 end |
|
141 elseif request.state == "headers" then |
|
142 log("debug", "Reading headers...") |
|
143 local pos = startpos; |
|
144 local headers = request.responseheaders or {}; |
|
145 for line in data:gmatch("(.-)\r\n") do |
|
146 startpos = (startpos or 1) + #line + 2; |
|
147 local k, v = line:match("(%S+): (.+)"); |
|
148 if k and v then |
|
149 headers[k:lower()] = v; |
|
150 -- log("debug", "Header: "..k:lower().." = "..v); |
|
151 elseif #line == 0 then |
|
152 request.responseheaders = headers; |
|
153 break; |
|
154 else |
|
155 log("debug", "Unhandled header line: "..line); |
|
156 end |
|
157 end |
|
158 |
|
159 if not expectbody(request) then |
|
160 call_callback(request); |
|
161 return; |
|
162 end |
|
163 |
|
164 -- Reached the end of the headers |
|
165 request.state = "body"; |
|
166 if #data > startpos then |
|
167 return request_reader(request, data:sub(startpos, -1)); |
|
168 end |
|
169 elseif request.state == "request" then |
|
170 log("debug", "Reading request line...") |
|
171 local method, path, http, linelen = data:match("^(%S+) (%S+) HTTP/(%S+)\r\n()", startpos); |
|
172 if not method then |
|
173 return call_callback(request, "invalid-status-line"); |
|
174 end |
|
175 |
|
176 request.method, request.path, request.httpversion = method, path, http; |
|
177 |
|
178 request.url = url_parse(request.path); |
|
179 |
|
180 log("debug", method.." request for "..tostring(request.path) .. " on port "..request.handler.serverport()); |
|
181 |
|
182 if request.onlystatus then |
|
183 if not call_callback(request) then |
|
184 return; |
|
185 end |
|
186 end |
|
187 |
|
188 request.state = "headers"; |
|
189 |
|
190 if #data > linelen then |
|
191 return request_reader(request, data:sub(linelen, -1)); |
|
192 end |
|
193 end |
|
194 end |
|
195 |
|
196 -- The default handler for requests |
|
197 default_handler = function (method, body, request) |
|
198 log("debug", method.." request for "..tostring(request.path) .. " on port "..request.handler.serverport()); |
|
199 return { status = "404 Not Found", |
|
200 headers = { ["Content-Type"] = "text/html" }, |
|
201 body = "<html><head><title>Page Not Found</title></head><body>Not here :(</body></html>" }; |
|
202 end |
|
203 |
|
204 |
|
205 function new_request(handler) |
|
206 return { handler = handler, conn = handler.socket, |
|
207 write = handler.write, state = "request", |
|
208 server = http_servers[handler.serverport()], |
|
209 send = send_response, |
|
210 destroy = destroy_request, |
|
211 id = tostring{}:match("%x+$") |
|
212 }; |
|
213 end |
|
214 |
|
215 function destroy_request(request) |
|
216 log("debug", "Destroying request %s", request.id); |
|
217 listener = listener or connlisteners_get("httpserver"); |
|
218 if not request.destroyed then |
|
219 request.destroyed = true; |
|
220 if request.on_destroy then |
|
221 log("debug", "Request has destroy callback"); |
|
222 request.on_destroy(request); |
|
223 else |
|
224 log("debug", "Request has no destroy callback"); |
|
225 end |
|
226 request.handler.close() |
|
227 if request.conn then |
|
228 listener.disconnect(request.conn, "closed"); |
|
229 end |
|
230 end |
|
231 end |
|
232 |
|
233 function new(params) |
|
234 local http_server = http_servers[params.port]; |
|
235 if not http_server then |
|
236 http_server = { handlers = {} }; |
|
237 http_servers[params.port] = http_server; |
|
238 -- We weren't already listening on this port, so start now |
|
239 connlisteners_start("httpserver", params); |
|
240 end |
|
241 if params.base then |
|
242 http_server.handlers[params.base] = params.handler; |
|
243 end |
|
244 end |
|
245 |
|
246 _M.request_reader = request_reader; |
|
247 _M.send_response = send_response; |
|
248 _M.urlencode = urlencode; |
|
249 |
|
250 return _M; |