lucid: fix separation of query string and url in server handler
[project/luci.git] / libs / lucid-http / luasrc / lucid / http / server.lua
1 --[[
2 LuCId HTTP-Slave
3 (c) 2009 Steven Barth <steven@midlink.org>
4
5 Licensed under the Apache License, Version 2.0 (the "License");
6 you may not use this file except in compliance with the License.
7 You may obtain a copy of the License at
8
9 http://www.apache.org/licenses/LICENSE-2.0
10
11 $Id$
12 ]]--
13
14 local ipairs, pairs = ipairs, pairs
15 local tostring, tonumber = tostring, tonumber
16 local pcall, assert, type = pcall, assert, type
17
18 local os = require "os"
19 local nixio = require "nixio"
20 local util = require "luci.util"
21 local ltn12 = require "luci.ltn12"
22 local proto = require "luci.http.protocol"
23 local table = require "table"
24 local date = require "luci.http.protocol.date"
25
26 module "luci.lucid.http.server"
27
28 VERSION = "1.0"
29
30 statusmsg = {
31 [200] = "OK",
32 [206] = "Partial Content",
33 [301] = "Moved Permanently",
34 [302] = "Found",
35 [304] = "Not Modified",
36 [400] = "Bad Request",
37 [401] = "Unauthorized",
38 [403] = "Forbidden",
39 [404] = "Not Found",
40 [405] = "Method Not Allowed",
41 [408] = "Request Time-out",
42 [411] = "Length Required",
43 [412] = "Precondition Failed",
44 [416] = "Requested range not satisfiable",
45 [500] = "Internal Server Error",
46 [503] = "Server Unavailable",
47 }
48
49 -- File Resource
50 IOResource = util.class()
51
52 function IOResource.__init__(self, fd, len)
53 self.fd, self.len = fd, len
54 end
55
56
57 -- Server handler implementation
58 Handler = util.class()
59
60 function Handler.__init__(self, name)
61 self.name = name or tostring(self)
62 end
63
64 -- Creates a failure reply
65 function Handler.failure(self, code, msg)
66 return code, { ["Content-Type"] = "text/plain" }, ltn12.source.string(msg)
67 end
68
69 -- Access Restrictions
70 function Handler.restrict(self, restriction)
71 if not self.restrictions then
72 self.restrictions = {restriction}
73 else
74 self.restrictions[#self.restrictions+1] = restriction
75 end
76 end
77
78 -- Check restrictions
79 function Handler.checkrestricted(self, request)
80 if not self.restrictions then
81 return
82 end
83
84 local localif, user, pass
85
86 for _, r in ipairs(self.restrictions) do
87 local stat = true
88 if stat and r.interface then -- Interface restriction
89 if not localif then
90 for _, v in ipairs(request.server.interfaces) do
91 if v.addr == request.env.SERVER_ADDR then
92 localif = v.name
93 break
94 end
95 end
96 end
97
98 if r.interface ~= localif then
99 stat = false
100 end
101 end
102
103 if stat and r.user then -- User restriction
104 local rh, pwe
105 if not user then
106 rh = (request.headers.Authorization or ""):match("Basic (.*)")
107 rh = rh and nixio.bin.b64decode(rh) or ""
108 user, pass = rh:match("(.*):(.*)")
109 pass = pass or ""
110 end
111 pwe = nixio.getsp and nixio.getsp(r.user) or nixio.getpw(r.user)
112 local pwh = (user == r.user) and pwe and (pwe.pwdp or pwe.passwd)
113 if not pwh or #pwh < 1 or nixio.crypt(pass, pwh) ~= pwh then
114 stat = false
115 end
116 end
117
118 if stat then
119 return
120 end
121 end
122
123 return 401, {
124 ["WWW-Authenticate"] = ('Basic realm=%q'):format(self.name),
125 ["Content-Type"] = 'text/plain'
126 }, ltn12.source.string("Unauthorized")
127 end
128
129 -- Processes a request
130 function Handler.process(self, request, sourcein)
131 local stat, code, hdr, sourceout
132
133 local stat, code, msg = self:checkrestricted(request)
134 if stat then -- Access Denied
135 return stat, code, msg
136 end
137
138 -- Detect request Method
139 local hname = "handle_" .. request.env.REQUEST_METHOD
140 if self[hname] then
141 -- Run the handler
142 stat, code, hdr, sourceout = pcall(self[hname], self, request, sourcein)
143
144 -- Check for any errors
145 if not stat then
146 return self:failure(500, code)
147 end
148 else
149 return self:failure(405, statusmsg[405])
150 end
151
152 return code, hdr, sourceout
153 end
154
155
156 VHost = util.class()
157
158 function VHost.__init__(self)
159 self.handlers = {}
160 end
161
162 function VHost.process(self, request, ...)
163 local handler
164 local hlen = -1
165 local uri = request.env.SCRIPT_NAME
166 local sc = ("/"):byte()
167
168 -- SCRIPT_NAME
169 request.env.SCRIPT_NAME = ""
170
171 -- Call URI part
172 request.env.PATH_INFO = uri
173
174 for k, h in pairs(self.handlers) do
175 if #k > hlen then
176 if uri == k or (uri:sub(1, #k) == k and uri:byte(#k+1) == sc) then
177 handler = h
178 hlen = #k
179 request.env.SCRIPT_NAME = k
180 request.env.PATH_INFO = uri:sub(#k+1)
181 end
182 end
183 end
184
185 if handler then
186 return handler:process(request, ...)
187 else
188 return 404, nil, ltn12.source.string("No such handler")
189 end
190 end
191
192 function VHost.get_handlers(self)
193 return self.handlers
194 end
195
196 function VHost.set_handler(self, match, handler)
197 self.handlers[match] = handler
198 end
199
200
201 local function remapipv6(adr)
202 local map = "::ffff:"
203 if adr:sub(1, #map) == map then
204 return adr:sub(#map+1)
205 else
206 return adr
207 end
208 end
209
210 local function chunksource(sock, buffer)
211 buffer = buffer or ""
212 return function()
213 local output
214 local _, endp, count = buffer:find("^([0-9a-fA-F]+);?.-\r\n")
215 while not count and #buffer <= 1024 do
216 local newblock, code = sock:recv(1024 - #buffer)
217 if not newblock then
218 return nil, code
219 end
220 buffer = buffer .. newblock
221 _, endp, count = buffer:find("^([0-9a-fA-F]+);?.-\r\n")
222 end
223 count = tonumber(count, 16)
224 if not count then
225 return nil, -1, "invalid encoding"
226 elseif count == 0 then
227 return nil
228 elseif count + 2 <= #buffer - endp then
229 output = buffer:sub(endp+1, endp+count)
230 buffer = buffer:sub(endp+count+3)
231 return output
232 else
233 output = buffer:sub(endp+1, endp+count)
234 buffer = ""
235 if count - #output > 0 then
236 local remain, code = sock:recvall(count-#output)
237 if not remain then
238 return nil, code
239 end
240 output = output .. remain
241 count, code = sock:recvall(2)
242 else
243 count, code = sock:recvall(count+2-#buffer+endp)
244 end
245 if not count then
246 return nil, code
247 end
248 return output
249 end
250 end
251 end
252
253 local function chunksink(sock)
254 return function(chunk, err)
255 if not chunk then
256 return sock:writeall("0\r\n\r\n")
257 else
258 return sock:writeall(("%X\r\n%s\r\n"):format(#chunk, chunk))
259 end
260 end
261 end
262
263 Server = util.class()
264
265 function Server.__init__(self)
266 self.vhosts = {}
267 end
268
269 function Server.get_vhosts(self)
270 return self.vhosts
271 end
272
273 function Server.set_vhost(self, name, vhost)
274 self.vhosts[name] = vhost
275 end
276
277 function Server.error(self, client, code, msg)
278 hcode = tostring(code)
279
280 client:writeall( "HTTP/1.0 " .. hcode .. " " ..
281 statusmsg[code] .. "\r\n" )
282 client:writeall( "Connection: close\r\n" )
283 client:writeall( "Content-Type: text/plain\r\n\r\n" )
284
285 if msg then
286 client:writeall( "HTTP-Error " .. code .. ": " .. msg .. "\r\n" )
287 end
288
289 client:close()
290 end
291
292 local hdr2env = {
293 ["Content-Length"] = "CONTENT_LENGTH",
294 ["Content-Type"] = "CONTENT_TYPE",
295 ["Content-type"] = "CONTENT_TYPE",
296 ["Accept"] = "HTTP_ACCEPT",
297 ["Accept-Charset"] = "HTTP_ACCEPT_CHARSET",
298 ["Accept-Encoding"] = "HTTP_ACCEPT_ENCODING",
299 ["Accept-Language"] = "HTTP_ACCEPT_LANGUAGE",
300 ["Connection"] = "HTTP_CONNECTION",
301 ["Cookie"] = "HTTP_COOKIE",
302 ["Host"] = "HTTP_HOST",
303 ["Referer"] = "HTTP_REFERER",
304 ["User-Agent"] = "HTTP_USER_AGENT"
305 }
306
307 function Server.parse_headers(self, source)
308 local env = {}
309 local req = {env = env, headers = {}}
310 local line, err
311
312 repeat -- Ignore empty lines
313 line, err = source()
314 if not line then
315 return nil, err
316 end
317 until #line > 0
318
319 env.REQUEST_METHOD, env.REQUEST_URI, env.SERVER_PROTOCOL =
320 line:match("^([A-Z]+) ([^ ]+) (HTTP/1%.[01])$")
321
322 if not env.REQUEST_METHOD then
323 return nil, "invalid magic"
324 end
325
326 local key, envkey, val
327 repeat
328 line, err = source()
329 if not line then
330 return nil, err
331 elseif #line > 0 then
332 key, val = line:match("^([%w-]+)%s?:%s?(.*)")
333 if key then
334 req.headers[key] = val
335 envkey = hdr2env[key]
336 if envkey then
337 env[envkey] = val
338 end
339 else
340 return nil, "invalid header line"
341 end
342 else
343 break
344 end
345 until false
346
347 if env.REQUEST_URI:find("?") then
348 env.SCRIPT_NAME, env.QUERY_STRING = env.REQUEST_URI:match("([^%?]*)%?(.*)")
349 else
350 env.SCRIPT_NAME, env.QUERY_STRING = env.REQUEST_URI, nil
351 end
352
353 return req
354 end
355
356
357 function Server.process(self, client, env)
358 local sourcein = function() end
359 local sourcehdr = client:linesource()
360 local sinkout
361 local buffer
362
363 local close = false
364 local stat, code, msg, message, err
365
366 client:setsockopt("socket", "rcvtimeo", 5)
367 client:setsockopt("socket", "sndtimeo", 5)
368
369 repeat
370 -- parse headers
371 message, err = self:parse_headers(sourcehdr)
372
373 -- any other error
374 if not message or err then
375 if err == 11 then -- EAGAIN
376 break
377 else
378 return self:error(client, 400, err)
379 end
380 end
381
382 -- Prepare sources and sinks
383 buffer = sourcehdr(true)
384 sinkout = client:sink()
385 message.server = env
386
387 if client:is_tls_socket() then
388 message.env.HTTPS = "on"
389 end
390
391 -- Addresses
392 message.env.REMOTE_ADDR = remapipv6(env.host)
393 message.env.REMOTE_PORT = env.port
394
395 local srvaddr, srvport = client:getsockname()
396 message.env.SERVER_ADDR = remapipv6(srvaddr)
397 message.env.SERVER_PORT = srvport
398
399 -- keep-alive
400 if message.env.SERVER_PROTOCOL == "HTTP/1.1" then
401 close = (message.env.HTTP_CONNECTION == "close")
402 else
403 close = not message.env.HTTP_CONNECTION
404 or message.env.HTTP_CONNECTION == "close"
405 end
406
407 -- Uncomment this to disable keep-alive
408 close = close or env.config.nokeepalive
409
410 if message.env.REQUEST_METHOD == "GET"
411 or message.env.REQUEST_METHOD == "HEAD" then
412 -- Be happy
413
414 elseif message.env.REQUEST_METHOD == "POST" then
415 -- If we have a HTTP/1.1 client and an Expect: 100-continue header
416 -- respond with HTTP 100 Continue message
417 if message.env.SERVER_PROTOCOL == "HTTP/1.1"
418 and message.headers.Expect == '100-continue' then
419 client:writeall("HTTP/1.1 100 Continue\r\n\r\n")
420 end
421
422 if message.headers['Transfer-Encoding'] and
423 message.headers['Transfer-Encoding'] ~= "identity" then
424 sourcein = chunksource(client, buffer)
425 buffer = nil
426 elseif message.env.CONTENT_LENGTH then
427 local len = tonumber(message.env.CONTENT_LENGTH)
428 if #buffer >= len then
429 sourcein = ltn12.source.string(buffer:sub(1, len))
430 buffer = buffer:sub(len+1)
431 else
432 sourcein = ltn12.source.cat(
433 ltn12.source.string(buffer),
434 client:blocksource(nil, len - #buffer)
435 )
436 end
437 else
438 return self:error(client, 411, statusmsg[411])
439 end
440 else
441 return self:error(client, 405, statusmsg[405])
442 end
443
444
445 local host = self.vhosts[message.env.HTTP_HOST] or self.vhosts[""]
446 if not host then
447 return self:error(client, 404, "No virtual host found")
448 end
449
450 local code, headers, sourceout = host:process(message, sourcein)
451 headers = headers or {}
452
453 -- Post process response
454 if sourceout then
455 if util.instanceof(sourceout, IOResource) then
456 if not headers["Content-Length"] then
457 headers["Content-Length"] = sourceout.len
458 end
459 end
460 if not headers["Content-Length"] then
461 if message.env.SERVER_PROTOCOL == "HTTP/1.1" then
462 headers["Transfer-Encoding"] = "chunked"
463 sinkout = chunksink(client)
464 else
465 close = true
466 end
467 end
468 elseif message.env.REQUEST_METHOD ~= "HEAD" then
469 headers["Content-Length"] = 0
470 end
471
472 if close then
473 headers["Connection"] = "close"
474 elseif message.env.SERVER_PROTOCOL == "HTTP/1.0" then
475 headers["Connection"] = "Keep-Alive"
476 end
477
478 headers["Date"] = date.to_http(os.time())
479 local header = {
480 message.env.SERVER_PROTOCOL .. " " .. tostring(code) .. " "
481 .. statusmsg[code],
482 "Server: LuCId-HTTPd/" .. VERSION
483 }
484
485
486 for k, v in pairs(headers) do
487 if type(v) == "table" then
488 for _, h in ipairs(v) do
489 header[#header+1] = k .. ": " .. h
490 end
491 else
492 header[#header+1] = k .. ": " .. v
493 end
494 end
495
496 header[#header+1] = ""
497 header[#header+1] = ""
498
499 -- Output
500 stat, code, msg = client:writeall(table.concat(header, "\r\n"))
501
502 if sourceout and stat then
503 if util.instanceof(sourceout, IOResource) then
504 stat, code, msg = sourceout.fd:copyz(client, sourceout.len)
505 else
506 stat, msg = ltn12.pump.all(sourceout, sinkout)
507 end
508 end
509
510
511 -- Write errors
512 if not stat then
513 if msg then
514 nixio.syslog("err", "Error sending data to " .. env.host ..
515 ": " .. msg .. "\n")
516 end
517 break
518 end
519
520 if buffer then
521 sourcehdr(buffer)
522 end
523 until close
524
525 client:shutdown()
526 client:close()
527 end