Avoid lacking fds
[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 local set_memory_limit = set_memory_limit
18
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"
26
27 --- HTTP Daemon
28 -- @cstyle instance
29 module "luci.lucid.http.server"
30
31 VERSION = "1.0"
32
33 statusmsg = {
34 [200] = "OK",
35 [206] = "Partial Content",
36 [301] = "Moved Permanently",
37 [302] = "Found",
38 [304] = "Not Modified",
39 [400] = "Bad Request",
40 [401] = "Unauthorized",
41 [403] = "Forbidden",
42 [404] = "Not Found",
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",
50 }
51
52 --- Create a new IO resource response.
53 -- @class function
54 -- @param fd File descriptor
55 -- @param len Length of data
56 -- @return IO resource
57 IOResource = util.class()
58
59 function IOResource.__init__(self, fd, len)
60 self.fd, self.len = fd, len
61 end
62
63
64 --- Create a server handler.
65 -- @class function
66 -- @param name Name
67 -- @return Handler
68 Handler = util.class()
69
70 function Handler.__init__(self, name)
71 self.name = name or tostring(self)
72 end
73
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)
80 end
81
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}
87 else
88 self.restrictions[#self.restrictions+1] = restriction
89 end
90 end
91
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
97 return
98 end
99
100 local localif, user, pass
101
102 for _, r in ipairs(self.restrictions) do
103 local stat = true
104 if stat and r.interface then -- Interface restriction
105 if not localif then
106 for _, v in ipairs(request.server.interfaces) do
107 if v.addr == request.env.SERVER_ADDR then
108 localif = v.name
109 break
110 end
111 end
112 end
113
114 if r.interface ~= localif then
115 stat = false
116 end
117 end
118
119 if stat and r.user then -- User restriction
120 local rh, pwe
121 if not user then
122 rh = (request.headers.Authorization or ""):match("Basic (.*)")
123 rh = rh and nixio.bin.b64decode(rh) or ""
124 user, pass = rh:match("(.*):(.*)")
125 pass = pass or ""
126 end
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
130 stat = false
131 end
132 end
133
134 if stat then
135 request.env.HTTP_AUTH_USER, request.env.HTTP_AUTH_PASS = user, pass
136 return
137 end
138 end
139
140 return 401, {
141 ["WWW-Authenticate"] = ('Basic realm=%q'):format(self.name),
142 ["Content-Type"] = 'text/plain'
143 }, ltn12.source.string("Unauthorized")
144 end
145
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
152
153 local stat, code, msg = self:checkrestricted(request)
154 if stat then -- Access Denied
155 return stat, code, msg
156 end
157
158 -- Detect request Method
159 local hname = "handle_" .. request.env.REQUEST_METHOD
160 if self[hname] then
161 -- Run the handler
162 stat, code, hdr, sourceout = pcall(self[hname], self, request, sourcein)
163
164 -- Check for any errors
165 if not stat then
166 return self:failure(500, code)
167 end
168 else
169 return self:failure(405, statusmsg[405])
170 end
171
172 return code, hdr, sourceout
173 end
174
175
176 --- Create a Virtual Host.
177 -- @class function
178 -- @return Virtual Host
179 VHost = util.class()
180
181 function VHost.__init__(self)
182 self.handlers = {}
183 end
184
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, ...)
190 local handler
191 local hlen = -1
192 local uri = request.env.SCRIPT_NAME
193 local sc = ("/"):byte()
194
195 -- SCRIPT_NAME
196 request.env.SCRIPT_NAME = ""
197
198 -- Call URI part
199 request.env.PATH_INFO = uri
200
201 if self.default and uri == "/" then
202 return 302, {Location = self.default}
203 end
204
205 for k, h in pairs(self.handlers) do
206 if #k > hlen then
207 if uri == k or (uri:sub(1, #k) == k and uri:byte(#k+1) == sc) then
208 handler = h
209 hlen = #k
210 request.env.SCRIPT_NAME = k
211 request.env.PATH_INFO = uri:sub(#k+1)
212 end
213 end
214 end
215
216 if handler then
217 return handler:process(request, ...)
218 else
219 return 404, nil, ltn12.source.string("No such handler")
220 end
221 end
222
223 --- Get a list of registered handlers.
224 -- @return Table of handlers
225 function VHost.get_handlers(self)
226 return self.handlers
227 end
228
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
234 end
235
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)
241 else
242 return adr
243 end
244 end
245
246 -- Create a source that decodes chunked-encoded data from a socket.
247 local function chunksource(sock, buffer)
248 buffer = buffer or ""
249 return function()
250 local output
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)
254 if not newblock then
255 return nil, code
256 end
257 buffer = buffer .. newblock
258 _, endp, count = buffer:find("^([0-9a-fA-F]+);?.-\r\n")
259 end
260 count = tonumber(count, 16)
261 if not count then
262 return nil, -1, "invalid encoding"
263 elseif count == 0 then
264 return nil
265 elseif count + 2 <= #buffer - endp then
266 output = buffer:sub(endp+1, endp+count)
267 buffer = buffer:sub(endp+count+3)
268 return output
269 else
270 output = buffer:sub(endp+1, endp+count)
271 buffer = ""
272 if count - #output > 0 then
273 local remain, code = sock:recvall(count-#output)
274 if not remain then
275 return nil, code
276 end
277 output = output .. remain
278 count, code = sock:recvall(2)
279 else
280 count, code = sock:recvall(count+2-#buffer+endp)
281 end
282 if not count then
283 return nil, code
284 end
285 return output
286 end
287 end
288 end
289
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)
293 if not chunk then
294 return sock:writeall("0\r\n\r\n")
295 else
296 return sock:writeall(("%X\r\n%s\r\n"):format(#chunk, tostring(chunk)))
297 end
298 end
299 end
300
301
302 --- Create a server object.
303 -- @class function
304 -- @return Server object
305 Server = util.class()
306
307 function Server.__init__(self)
308 self.vhosts = {}
309 end
310
311 --- Get a list of registered virtual hosts.
312 -- @return Table of virtual hosts
313 function Server.get_vhosts(self)
314 return self.vhosts
315 end
316
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
322 end
323
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)
330
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" )
335
336 if msg then
337 client:writeall( "HTTP-Error " .. code .. ": " .. msg .. "\r\n" )
338 end
339
340 client:close()
341 end
342
343 local hdr2env = {
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"
356 }
357
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)
362 local env = {}
363 local req = {env = env, headers = {}}
364 local line, err
365
366 repeat -- Ignore empty lines
367 line, err = source()
368 if not line then
369 return nil, err
370 end
371 until #line > 0
372
373 env.REQUEST_METHOD, env.REQUEST_URI, env.SERVER_PROTOCOL =
374 line:match("^([A-Z]+) ([^ ]+) (HTTP/1%.[01])$")
375
376 if not env.REQUEST_METHOD then
377 return nil, "invalid magic"
378 end
379
380 local key, envkey, val
381 repeat
382 line, err = source()
383 if not line then
384 return nil, err
385 elseif #line > 0 then
386 key, val = line:match("^([%w-]+)%s?:%s?(.*)")
387 if key then
388 req.headers[key] = val
389 envkey = hdr2env[key]
390 if envkey then
391 env[envkey] = val
392 end
393 else
394 return nil, "invalid header line"
395 end
396 else
397 break
398 end
399 until false
400
401 env.SCRIPT_NAME, env.QUERY_STRING = env.REQUEST_URI:match("([^?]*)%??(.*)")
402 return req
403 end
404
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()
411 local sinkout
412 local buffer
413
414 local close = false
415 local stat, code, msg, message, err
416
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)
420 end
421
422 client:setsockopt("socket", "rcvtimeo", 5)
423 client:setsockopt("socket", "sndtimeo", 5)
424
425 repeat
426 -- parse headers
427 message, err = self:parse_headers(sourcehdr)
428
429 -- any other error
430 if not message or err then
431 if err == 11 then -- EAGAIN
432 break
433 else
434 return self:error(client, 400, err)
435 end
436 end
437
438 -- Prepare sources and sinks
439 buffer = sourcehdr(true)
440 sinkout = client:sink()
441 message.server = env
442
443 if client:is_tls_socket() then
444 message.env.HTTPS = "on"
445 end
446
447 -- Addresses
448 message.env.REMOTE_ADDR = remapipv6(env.host)
449 message.env.REMOTE_PORT = env.port
450
451 local srvaddr, srvport = client:getsockname()
452 message.env.SERVER_ADDR = remapipv6(srvaddr)
453 message.env.SERVER_PORT = srvport
454
455 -- keep-alive
456 if message.env.SERVER_PROTOCOL == "HTTP/1.1" then
457 close = (message.env.HTTP_CONNECTION == "close")
458 else
459 close = not message.env.HTTP_CONNECTION
460 or message.env.HTTP_CONNECTION == "close"
461 end
462
463 -- Uncomment this to disable keep-alive
464 close = close or env.config.nokeepalive
465
466 if message.env.REQUEST_METHOD == "GET"
467 or message.env.REQUEST_METHOD == "HEAD" then
468 -- Be happy
469
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")
476 end
477
478 if message.headers['Transfer-Encoding'] and
479 message.headers['Transfer-Encoding'] ~= "identity" then
480 sourcein = chunksource(client, buffer)
481 buffer = nil
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)
487 else
488 sourcein = ltn12.source.cat(
489 ltn12.source.string(buffer),
490 client:blocksource(nil, len - #buffer)
491 )
492 end
493 else
494 return self:error(client, 411, statusmsg[411])
495 end
496
497 close = true
498 else
499 return self:error(client, 405, statusmsg[405])
500 end
501
502
503 local host = self.vhosts[message.env.HTTP_HOST] or self.vhosts[""]
504 if not host then
505 return self:error(client, 404, "No virtual host found")
506 end
507
508 local code, headers, sourceout = host:process(message, sourcein)
509 headers = headers or {}
510
511 -- Post process response
512 if sourceout then
513 if util.instanceof(sourceout, IOResource) then
514 if not headers["Content-Length"] then
515 headers["Content-Length"] = sourceout.len
516 end
517 end
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)
522 else
523 close = true
524 end
525 end
526 elseif message.env.REQUEST_METHOD ~= "HEAD" then
527 headers["Content-Length"] = 0
528 end
529
530 if close then
531 headers["Connection"] = "close"
532 else
533 headers["Connection"] = "Keep-Alive"
534 headers["Keep-Alive"] = "timeout=5, max=50"
535 end
536
537 headers["Date"] = date.to_http(os.time())
538 local header = {
539 message.env.SERVER_PROTOCOL .. " " .. tostring(code) .. " "
540 .. statusmsg[code],
541 "Server: LuCId-HTTPd/" .. VERSION
542 }
543
544
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
549 end
550 else
551 header[#header+1] = k .. ": " .. v
552 end
553 end
554
555 header[#header+1] = ""
556 header[#header+1] = ""
557
558 -- Output
559 stat, code, msg = client:writeall(table.concat(header, "\r\n"))
560
561 if sourceout and stat then
562 local closefd
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
567 sourceout = nil
568 else
569 closefd = sourceout.fd
570 sourceout = sourceout.fd:blocksource(nil, sourceout.len)
571 end
572 end
573
574 if sourceout then
575 stat, msg = ltn12.pump.all(sourceout, sinkout)
576 end
577
578 if closefd then
579 closefd:close()
580 end
581 end
582
583
584 -- Write errors
585 if not stat then
586 if msg then
587 nixio.syslog("err", "Error sending data to " .. env.host ..
588 ": " .. msg .. "\n")
589 end
590 break
591 end
592
593 if buffer then
594 sourcehdr(buffer)
595 end
596 until close
597
598 client:shutdown()
599 client:close()
600 end