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
17 local set_memory_limit = set_memory_limit
19 local os = require "os"
20 local nixio = require "nixio"
21 local util = require "luci.util"
22 local ltn12 = require "luci.ltn12"
23 local proto = require "luci.http.protocol"
24 local table = require "table"
25 local date = require "luci.http.protocol.date"
29 module "luci.lucid.http.server"
35 [206] = "Partial Content",
36 [301] = "Moved Permanently",
38 [304] = "Not Modified",
39 [400] = "Bad Request",
40 [401] = "Unauthorized",
43 [405] = "Method Not Allowed",
44 [408] = "Request Time-out",
45 [411] = "Length Required",
46 [412] = "Precondition Failed",
47 [416] = "Requested range not satisfiable",
48 [500] = "Internal Server Error",
49 [503] = "Server Unavailable",
52 --- Create a new IO resource response.
54 -- @param fd File descriptor
55 -- @param len Length of data
56 -- @return IO resource
57 IOResource = util.class()
59 function IOResource.__init__(self, fd, len)
60 self.fd, self.len = fd, len
64 --- Create a server handler.
68 Handler = util.class()
70 function Handler.__init__(self, name)
71 self.name = name or tostring(self)
74 --- Create a failure reply.
75 -- @param code HTTP status code
76 -- @param msg Status message
77 -- @return status code, header table, response source
78 function Handler.failure(self, code, msg)
79 return code, { ["Content-Type"] = "text/plain" }, ltn12.source.string(msg)
82 --- Add an access restriction.
83 -- @param restriction Restriction specification
84 function Handler.restrict(self, restriction)
85 if not self.restrictions then
86 self.restrictions = {restriction}
88 self.restrictions[#self.restrictions+1] = restriction
92 --- Enforce access restrictions.
93 -- @param request Request object
94 -- @return nil or HTTP statuscode, table of headers, response source
95 function Handler.checkrestricted(self, request)
96 if not self.restrictions then
100 local localif, user, pass
102 for _, r in ipairs(self.restrictions) do
104 if stat and r.interface then -- Interface restriction
106 for _, v in ipairs(request.server.interfaces) do
107 if v.addr == request.env.SERVER_ADDR then
114 if r.interface ~= localif then
119 if stat and r.user then -- User restriction
122 rh = (request.headers.Authorization or ""):match("Basic (.*)")
123 rh = rh and nixio.bin.b64decode(rh) or ""
124 user, pass = rh:match("(.*):(.*)")
127 pwe = nixio.getsp and nixio.getsp(r.user) or nixio.getpw(r.user)
128 local pwh = (user == r.user) and pwe and (pwe.pwdp or pwe.passwd)
129 if not pwh or #pwh < 1 or nixio.crypt(pass, pwh) ~= pwh then
135 request.env.HTTP_AUTH_USER, request.env.HTTP_AUTH_PASS = user, pass
141 ["WWW-Authenticate"] = ('Basic realm=%q'):format(self.name),
142 ["Content-Type"] = 'text/plain'
143 }, ltn12.source.string("Unauthorized")
146 --- Process a request.
147 -- @param request Request object
148 -- @param sourcein Request data source
149 -- @return HTTP statuscode, table of headers, response source
150 function Handler.process(self, request, sourcein)
151 local stat, code, hdr, sourceout
153 local stat, code, msg = self:checkrestricted(request)
154 if stat then -- Access Denied
155 return stat, code, msg
158 -- Detect request Method
159 local hname = "handle_" .. request.env.REQUEST_METHOD
162 stat, code, hdr, sourceout = pcall(self[hname], self, request, sourcein)
164 -- Check for any errors
166 return self:failure(500, code)
169 return self:failure(405, statusmsg[405])
172 return code, hdr, sourceout
176 --- Create a Virtual Host.
178 -- @return Virtual Host
181 function VHost.__init__(self)
185 --- Process a request and invoke the appropriate handler.
186 -- @param request Request object
187 -- @param ... Additional parameters passed to the handler
188 -- @return HTTP statuscode, table of headers, response source
189 function VHost.process(self, request, ...)
192 local uri = request.env.SCRIPT_NAME
193 local sc = ("/"):byte()
196 request.env.SCRIPT_NAME = ""
199 request.env.PATH_INFO = uri
201 if self.default and uri == "/" then
202 return 302, {Location = self.default}
205 for k, h in pairs(self.handlers) do
207 if uri == k or (uri:sub(1, #k) == k and uri:byte(#k+1) == sc) then
210 request.env.SCRIPT_NAME = k
211 request.env.PATH_INFO = uri:sub(#k+1)
217 return handler:process(request, ...)
219 return 404, nil, ltn12.source.string("No such handler")
223 --- Get a list of registered handlers.
224 -- @return Table of handlers
225 function VHost.get_handlers(self)
229 --- Register handler with a given URI prefix.
230 -- @oaram match URI prefix
231 -- @param handler Handler object
232 function VHost.set_handler(self, match, handler)
233 self.handlers[match] = handler
236 -- Remap IPv6-IPv4-compatibility addresses back to IPv4 addresses.
237 local function remapipv6(adr)
238 local map = "::ffff:"
239 if adr:sub(1, #map) == map then
240 return adr:sub(#map+1)
246 -- Create a source that decodes chunked-encoded data from a socket.
247 local function chunksource(sock, buffer)
248 buffer = buffer or ""
251 local _, endp, count = buffer:find("^([0-9a-fA-F]+);?.-\r\n")
252 while not count and #buffer <= 1024 do
253 local newblock, code = sock:recv(1024 - #buffer)
257 buffer = buffer .. newblock
258 _, endp, count = buffer:find("^([0-9a-fA-F]+);?.-\r\n")
260 count = tonumber(count, 16)
262 return nil, -1, "invalid encoding"
263 elseif count == 0 then
265 elseif count + 2 <= #buffer - endp then
266 output = buffer:sub(endp+1, endp+count)
267 buffer = buffer:sub(endp+count+3)
270 output = buffer:sub(endp+1, endp+count)
272 if count - #output > 0 then
273 local remain, code = sock:recvall(count-#output)
277 output = output .. remain
278 count, code = sock:recvall(2)
280 count, code = sock:recvall(count+2-#buffer+endp)
290 -- Create a sink that chunk-encodes data and writes it on a given socket.
291 local function chunksink(sock)
292 return function(chunk, err)
294 return sock:writeall("0\r\n\r\n")
296 return sock:writeall(("%X\r\n%s\r\n"):format(#chunk, tostring(chunk)))
302 --- Create a server object.
304 -- @return Server object
305 Server = util.class()
307 function Server.__init__(self)
311 --- Get a list of registered virtual hosts.
312 -- @return Table of virtual hosts
313 function Server.get_vhosts(self)
317 --- Register a virtual host with a given name.
318 -- @param name Hostname
319 -- @param vhost Virtual host object
320 function Server.set_vhost(self, name, vhost)
321 self.vhosts[name] = vhost
324 --- Send a fatal error message to given client and close the connection.
325 -- @param client Client socket
326 -- @param code HTTP status code
327 -- @param msg status message
328 function Server.error(self, client, code, msg)
329 hcode = tostring(code)
331 client:writeall( "HTTP/1.0 " .. hcode .. " " ..
332 statusmsg[code] .. "\r\n" )
333 client:writeall( "Connection: close\r\n" )
334 client:writeall( "Content-Type: text/plain\r\n\r\n" )
337 client:writeall( "HTTP-Error " .. code .. ": " .. msg .. "\r\n" )
344 ["Content-Length"] = "CONTENT_LENGTH",
345 ["Content-Type"] = "CONTENT_TYPE",
346 ["Content-type"] = "CONTENT_TYPE",
347 ["Accept"] = "HTTP_ACCEPT",
348 ["Accept-Charset"] = "HTTP_ACCEPT_CHARSET",
349 ["Accept-Encoding"] = "HTTP_ACCEPT_ENCODING",
350 ["Accept-Language"] = "HTTP_ACCEPT_LANGUAGE",
351 ["Connection"] = "HTTP_CONNECTION",
352 ["Cookie"] = "HTTP_COOKIE",
353 ["Host"] = "HTTP_HOST",
354 ["Referer"] = "HTTP_REFERER",
355 ["User-Agent"] = "HTTP_USER_AGENT"
358 --- Parse the request headers and prepare the environment.
359 -- @param source line-based input source
360 -- @return Request object
361 function Server.parse_headers(self, source)
363 local req = {env = env, headers = {}}
366 repeat -- Ignore empty lines
373 env.REQUEST_METHOD, env.REQUEST_URI, env.SERVER_PROTOCOL =
374 line:match("^([A-Z]+) ([^ ]+) (HTTP/1%.[01])$")
376 if not env.REQUEST_METHOD then
377 return nil, "invalid magic"
380 local key, envkey, val
385 elseif #line > 0 then
386 key, val = line:match("^([%w-]+)%s?:%s?(.*)")
388 req.headers[key] = val
389 envkey = hdr2env[key]
394 return nil, "invalid header line"
401 env.SCRIPT_NAME, env.QUERY_STRING = env.REQUEST_URI:match("([^?]*)%??(.*)")
405 --- Handle a new client connection.
406 -- @param client client socket
407 -- @param env superserver environment
408 function Server.process(self, client, env)
409 local sourcein = function() end
410 local sourcehdr = client:linesource()
415 local stat, code, msg, message, err
417 env.config.memlimit = tonumber(env.config.memlimit)
418 if env.config.memlimit and set_memory_limit then
419 set_memory_limit(env.config.memlimit)
422 client:setsockopt("socket", "rcvtimeo", 5)
423 client:setsockopt("socket", "sndtimeo", 5)
427 message, err = self:parse_headers(sourcehdr)
430 if not message or err then
431 if err == 11 then -- EAGAIN
434 return self:error(client, 400, err)
438 -- Prepare sources and sinks
439 buffer = sourcehdr(true)
440 sinkout = client:sink()
443 if client:is_tls_socket() then
444 message.env.HTTPS = "on"
448 message.env.REMOTE_ADDR = remapipv6(env.host)
449 message.env.REMOTE_PORT = env.port
451 local srvaddr, srvport = client:getsockname()
452 message.env.SERVER_ADDR = remapipv6(srvaddr)
453 message.env.SERVER_PORT = srvport
456 if message.env.SERVER_PROTOCOL == "HTTP/1.1" then
457 close = (message.env.HTTP_CONNECTION == "close")
459 close = not message.env.HTTP_CONNECTION
460 or message.env.HTTP_CONNECTION == "close"
463 -- Uncomment this to disable keep-alive
464 close = close or env.config.nokeepalive
466 if message.env.REQUEST_METHOD == "GET"
467 or message.env.REQUEST_METHOD == "HEAD" then
470 elseif message.env.REQUEST_METHOD == "POST" then
471 -- If we have a HTTP/1.1 client and an Expect: 100-continue header
472 -- respond with HTTP 100 Continue message
473 if message.env.SERVER_PROTOCOL == "HTTP/1.1"
474 and message.headers.Expect == '100-continue' then
475 client:writeall("HTTP/1.1 100 Continue\r\n\r\n")
478 if message.headers['Transfer-Encoding'] and
479 message.headers['Transfer-Encoding'] ~= "identity" then
480 sourcein = chunksource(client, buffer)
482 elseif message.env.CONTENT_LENGTH then
483 local len = tonumber(message.env.CONTENT_LENGTH)
484 if #buffer >= len then
485 sourcein = ltn12.source.string(buffer:sub(1, len))
486 buffer = buffer:sub(len+1)
488 sourcein = ltn12.source.cat(
489 ltn12.source.string(buffer),
490 client:blocksource(nil, len - #buffer)
494 return self:error(client, 411, statusmsg[411])
499 return self:error(client, 405, statusmsg[405])
503 local host = self.vhosts[message.env.HTTP_HOST] or self.vhosts[""]
505 return self:error(client, 404, "No virtual host found")
508 local code, headers, sourceout = host:process(message, sourcein)
509 headers = headers or {}
511 -- Post process response
513 if util.instanceof(sourceout, IOResource) then
514 if not headers["Content-Length"] then
515 headers["Content-Length"] = sourceout.len
518 if not headers["Content-Length"] and not close then
519 if message.env.SERVER_PROTOCOL == "HTTP/1.1" then
520 headers["Transfer-Encoding"] = "chunked"
521 sinkout = chunksink(client)
526 elseif message.env.REQUEST_METHOD ~= "HEAD" then
527 headers["Content-Length"] = 0
531 headers["Connection"] = "close"
533 headers["Connection"] = "Keep-Alive"
534 headers["Keep-Alive"] = "timeout=5, max=50"
537 headers["Date"] = date.to_http(os.time())
539 message.env.SERVER_PROTOCOL .. " " .. tostring(code) .. " "
541 "Server: LuCId-HTTPd/" .. VERSION
545 for k, v in pairs(headers) do
546 if type(v) == "table" then
547 for _, h in ipairs(v) do
548 header[#header+1] = k .. ": " .. h
551 header[#header+1] = k .. ": " .. v
555 header[#header+1] = ""
556 header[#header+1] = ""
559 stat, code, msg = client:writeall(table.concat(header, "\r\n"))
561 if sourceout and stat then
563 if util.instanceof(sourceout, IOResource) then
564 if not headers["Transfer-Encoding"] then
565 stat, code, msg = sourceout.fd:copyz(client, sourceout.len)
566 closefd = sourceout.fd
569 closefd = sourceout.fd
570 sourceout = sourceout.fd:blocksource(nil, sourceout.len)
575 stat, msg = ltn12.pump.all(sourceout, sinkout)
587 nixio.syslog("err", "Error sending data to " .. env.host ..