3 (c) 2009 Steven Barth <steven@midlink.org>
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
9 http://www.apache.org/licenses/LICENSE-2.0
14 local ipairs, pairs = ipairs, pairs
15 local tostring, tonumber = tostring, tonumber
16 local pcall, assert, type = pcall, assert, type
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"
26 module "luci.lucid.http.server"
32 [206] = "Partial Content",
33 [301] = "Moved Permanently",
35 [304] = "Not Modified",
36 [400] = "Bad Request",
37 [401] = "Unauthorized",
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",
50 IOResource = util.class()
52 function IOResource.__init__(self, fd, len)
53 self.fd, self.len = fd, len
57 -- Server handler implementation
58 Handler = util.class()
60 function Handler.__init__(self, name)
61 self.name = name or tostring(self)
64 -- Creates a failure reply
65 function Handler.failure(self, code, msg)
66 return code, { ["Content-Type"] = "text/plain" }, ltn12.source.string(msg)
69 -- Access Restrictions
70 function Handler.restrict(self, restriction)
71 if not self.restrictions then
72 self.restrictions = {restriction}
74 self.restrictions[#self.restrictions+1] = restriction
79 function Handler.checkrestricted(self, request)
80 if not self.restrictions then
84 local localif, user, pass
86 for _, r in ipairs(self.restrictions) do
88 if stat and r.interface then -- Interface restriction
90 for _, v in ipairs(request.server.interfaces) do
91 if v.addr == request.env.SERVER_ADDR then
98 if r.interface ~= localif then
103 if stat and r.user then -- User restriction
106 rh = (request.headers.Authorization or ""):match("Basic (.*)")
107 rh = rh and nixio.bin.b64decode(rh) or ""
108 user, pass = rh:match("(.*):(.*)")
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
124 ["WWW-Authenticate"] = ('Basic realm=%q'):format(self.name),
125 ["Content-Type"] = 'text/plain'
126 }, ltn12.source.string("Unauthorized")
129 -- Processes a request
130 function Handler.process(self, request, sourcein)
131 local stat, code, hdr, sourceout
133 local stat, code, msg = self:checkrestricted(request)
134 if stat then -- Access Denied
135 return stat, code, msg
138 -- Detect request Method
139 local hname = "handle_" .. request.env.REQUEST_METHOD
142 stat, code, hdr, sourceout = pcall(self[hname], self, request, sourcein)
144 -- Check for any errors
146 return self:failure(500, code)
149 return self:failure(405, statusmsg[405])
152 return code, hdr, sourceout
158 function VHost.__init__(self)
162 function VHost.process(self, request, ...)
165 local uri = request.env.SCRIPT_NAME
166 local sc = ("/"):byte()
169 request.env.SCRIPT_NAME = ""
172 request.env.PATH_INFO = uri
174 for k, h in pairs(self.handlers) do
176 if uri == k or (uri:sub(1, #k) == k and uri:byte(#k+1) == sc) then
179 request.env.SCRIPT_NAME = k
180 request.env.PATH_INFO = uri:sub(#k+1)
186 return handler:process(request, ...)
188 return 404, nil, ltn12.source.string("No such handler")
192 function VHost.get_handlers(self)
196 function VHost.set_handler(self, match, handler)
197 self.handlers[match] = handler
201 local function remapipv6(adr)
202 local map = "::ffff:"
203 if adr:sub(1, #map) == map then
204 return adr:sub(#map+1)
210 local function chunksource(sock, buffer)
211 buffer = buffer or ""
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)
220 buffer = buffer .. newblock
221 _, endp, count = buffer:find("^([0-9a-fA-F]+);?.-\r\n")
223 count = tonumber(count, 16)
225 return nil, -1, "invalid encoding"
226 elseif count == 0 then
228 elseif count + 2 <= #buffer - endp then
229 output = buffer:sub(endp+1, endp+count)
230 buffer = buffer:sub(endp+count+3)
233 output = buffer:sub(endp+1, endp+count)
235 if count - #output > 0 then
236 local remain, code = sock:recvall(count-#output)
240 output = output .. remain
241 count, code = sock:recvall(2)
243 count, code = sock:recvall(count+2-#buffer+endp)
253 local function chunksink(sock)
254 return function(chunk, err)
256 return sock:writeall("0\r\n\r\n")
258 return sock:writeall(("%X\r\n%s\r\n"):format(#chunk, chunk))
263 Server = util.class()
265 function Server.__init__(self)
269 function Server.get_vhosts(self)
273 function Server.set_vhost(self, name, vhost)
274 self.vhosts[name] = vhost
277 function Server.error(self, client, code, msg)
278 hcode = tostring(code)
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" )
286 client:writeall( "HTTP-Error " .. code .. ": " .. msg .. "\r\n" )
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"
307 function Server.parse_headers(self, source)
309 local req = {env = env, headers = {}}
312 repeat -- Ignore empty lines
319 env.REQUEST_METHOD, env.REQUEST_URI, env.SERVER_PROTOCOL =
320 line:match("^([A-Z]+) ([^ ]+) (HTTP/1%.[01])$")
322 if not env.REQUEST_METHOD then
323 return nil, "invalid magic"
326 local key, envkey, val
331 elseif #line > 0 then
332 key, val = line:match("^([%w-]+)%s?:%s?(.*)")
334 req.headers[key] = val
335 envkey = hdr2env[key]
340 return nil, "invalid header line"
347 env.SCRIPT_NAME, env.QUERY_STRING = env.REQUEST_URI:match("([^?]*)%??(.*)")
352 function Server.process(self, client, env)
353 local sourcein = function() end
354 local sourcehdr = client:linesource()
359 local stat, code, msg, message, err
361 client:setsockopt("socket", "rcvtimeo", 5)
362 client:setsockopt("socket", "sndtimeo", 5)
366 message, err = self:parse_headers(sourcehdr)
369 if not message or err then
370 if err == 11 then -- EAGAIN
373 return self:error(client, 400, err)
377 -- Prepare sources and sinks
378 buffer = sourcehdr(true)
379 sinkout = client:sink()
382 if client:is_tls_socket() then
383 message.env.HTTPS = "on"
387 message.env.REMOTE_ADDR = remapipv6(env.host)
388 message.env.REMOTE_PORT = env.port
390 local srvaddr, srvport = client:getsockname()
391 message.env.SERVER_ADDR = remapipv6(srvaddr)
392 message.env.SERVER_PORT = srvport
395 if message.env.SERVER_PROTOCOL == "HTTP/1.1" then
396 close = (message.env.HTTP_CONNECTION == "close")
398 close = not message.env.HTTP_CONNECTION
399 or message.env.HTTP_CONNECTION == "close"
402 -- Uncomment this to disable keep-alive
403 close = close or env.config.nokeepalive
405 if message.env.REQUEST_METHOD == "GET"
406 or message.env.REQUEST_METHOD == "HEAD" then
409 elseif message.env.REQUEST_METHOD == "POST" then
410 -- If we have a HTTP/1.1 client and an Expect: 100-continue header
411 -- respond with HTTP 100 Continue message
412 if message.env.SERVER_PROTOCOL == "HTTP/1.1"
413 and message.headers.Expect == '100-continue' then
414 client:writeall("HTTP/1.1 100 Continue\r\n\r\n")
417 if message.headers['Transfer-Encoding'] and
418 message.headers['Transfer-Encoding'] ~= "identity" then
419 sourcein = chunksource(client, buffer)
421 elseif message.env.CONTENT_LENGTH then
422 local len = tonumber(message.env.CONTENT_LENGTH)
423 if #buffer >= len then
424 sourcein = ltn12.source.string(buffer:sub(1, len))
425 buffer = buffer:sub(len+1)
427 sourcein = ltn12.source.cat(
428 ltn12.source.string(buffer),
429 client:blocksource(nil, len - #buffer)
433 return self:error(client, 411, statusmsg[411])
438 return self:error(client, 405, statusmsg[405])
442 local host = self.vhosts[message.env.HTTP_HOST] or self.vhosts[""]
444 return self:error(client, 404, "No virtual host found")
447 local code, headers, sourceout = host:process(message, sourcein)
448 headers = headers or {}
450 -- Post process response
452 if util.instanceof(sourceout, IOResource) then
453 if not headers["Content-Length"] then
454 headers["Content-Length"] = sourceout.len
457 if not headers["Content-Length"] then
458 if message.env.SERVER_PROTOCOL == "HTTP/1.1" then
459 headers["Transfer-Encoding"] = "chunked"
460 sinkout = chunksink(client)
465 elseif message.env.REQUEST_METHOD ~= "HEAD" then
466 headers["Content-Length"] = 0
470 headers["Connection"] = "close"
471 elseif message.env.SERVER_PROTOCOL == "HTTP/1.0" then
472 headers["Connection"] = "Keep-Alive"
475 headers["Date"] = date.to_http(os.time())
477 message.env.SERVER_PROTOCOL .. " " .. tostring(code) .. " "
479 "Server: LuCId-HTTPd/" .. VERSION
483 for k, v in pairs(headers) do
484 if type(v) == "table" then
485 for _, h in ipairs(v) do
486 header[#header+1] = k .. ": " .. h
489 header[#header+1] = k .. ": " .. v
493 header[#header+1] = ""
494 header[#header+1] = ""
497 stat, code, msg = client:writeall(table.concat(header, "\r\n"))
499 if sourceout and stat then
500 if util.instanceof(sourceout, IOResource) then
501 stat, code, msg = sourceout.fd:copyz(client, sourceout.len)
503 stat, msg = ltn12.pump.all(sourceout, sinkout)
511 nixio.syslog("err", "Error sending data to " .. env.host ..