Move memory limit to lucid, reincrease thread limit
[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 return
136 end
137 end
138
139 return 401, {
140 ["WWW-Authenticate"] = ('Basic realm=%q'):format(self.name),
141 ["Content-Type"] = 'text/plain'
142 }, ltn12.source.string("Unauthorized")
143 end
144
145 --- Process a request.
146 -- @param request Request object
147 -- @param sourcein Request data source
148 -- @return HTTP statuscode, table of headers, response source
149 function Handler.process(self, request, sourcein)
150 local stat, code, hdr, sourceout
151
152 local stat, code, msg = self:checkrestricted(request)
153 if stat then -- Access Denied
154 return stat, code, msg
155 end
156
157 -- Detect request Method
158 local hname = "handle_" .. request.env.REQUEST_METHOD
159 if self[hname] then
160 -- Run the handler
161 stat, code, hdr, sourceout = pcall(self[hname], self, request, sourcein)
162
163 -- Check for any errors
164 if not stat then
165 return self:failure(500, code)
166 end
167 else
168 return self:failure(405, statusmsg[405])
169 end
170
171 return code, hdr, sourceout
172 end
173
174
175 --- Create a Virtual Host.
176 -- @class function
177 -- @return Virtual Host
178 VHost = util.class()
179
180 function VHost.__init__(self)
181 self.handlers = {}
182 end
183
184 --- Process a request and invoke the appropriate handler.
185 -- @param request Request object
186 -- @param ... Additional parameters passed to the handler
187 -- @return HTTP statuscode, table of headers, response source
188 function VHost.process(self, request, ...)
189 local handler
190 local hlen = -1
191 local uri = request.env.SCRIPT_NAME
192 local sc = ("/"):byte()
193
194 -- SCRIPT_NAME
195 request.env.SCRIPT_NAME = ""
196
197 -- Call URI part
198 request.env.PATH_INFO = uri
199
200 for k, h in pairs(self.handlers) do
201 if #k > hlen then
202 if uri == k or (uri:sub(1, #k) == k and uri:byte(#k+1) == sc) then
203 handler = h
204 hlen = #k
205 request.env.SCRIPT_NAME = k
206 request.env.PATH_INFO = uri:sub(#k+1)
207 end
208 end
209 end
210
211 if handler then
212 return handler:process(request, ...)
213 else
214 return 404, nil, ltn12.source.string("No such handler")
215 end
216 end
217
218 --- Get a list of registered handlers.
219 -- @return Table of handlers
220 function VHost.get_handlers(self)
221 return self.handlers
222 end
223
224 --- Register handler with a given URI prefix.
225 -- @oaram match URI prefix
226 -- @param handler Handler object
227 function VHost.set_handler(self, match, handler)
228 self.handlers[match] = handler
229 end
230
231 -- Remap IPv6-IPv4-compatibility addresses back to IPv4 addresses.
232 local function remapipv6(adr)
233 local map = "::ffff:"
234 if adr:sub(1, #map) == map then
235 return adr:sub(#map+1)
236 else
237 return adr
238 end
239 end
240
241 -- Create a source that decodes chunked-encoded data from a socket.
242 local function chunksource(sock, buffer)
243 buffer = buffer or ""
244 return function()
245 local output
246 local _, endp, count = buffer:find("^([0-9a-fA-F]+);?.-\r\n")
247 while not count and #buffer <= 1024 do
248 local newblock, code = sock:recv(1024 - #buffer)
249 if not newblock then
250 return nil, code
251 end
252 buffer = buffer .. newblock
253 _, endp, count = buffer:find("^([0-9a-fA-F]+);?.-\r\n")
254 end
255 count = tonumber(count, 16)
256 if not count then
257 return nil, -1, "invalid encoding"
258 elseif count == 0 then
259 return nil
260 elseif count + 2 <= #buffer - endp then
261 output = buffer:sub(endp+1, endp+count)
262 buffer = buffer:sub(endp+count+3)
263 return output
264 else
265 output = buffer:sub(endp+1, endp+count)
266 buffer = ""
267 if count - #output > 0 then
268 local remain, code = sock:recvall(count-#output)
269 if not remain then
270 return nil, code
271 end
272 output = output .. remain
273 count, code = sock:recvall(2)
274 else
275 count, code = sock:recvall(count+2-#buffer+endp)
276 end
277 if not count then
278 return nil, code
279 end
280 return output
281 end
282 end
283 end
284
285 -- Create a sink that chunk-encodes data and writes it on a given socket.
286 local function chunksink(sock)
287 return function(chunk, err)
288 if not chunk then
289 return sock:writeall("0\r\n\r\n")
290 else
291 return sock:writeall(("%X\r\n%s\r\n"):format(#chunk, chunk))
292 end
293 end
294 end
295
296
297 --- Create a server object.
298 -- @class function
299 -- @return Server object
300 Server = util.class()
301
302 function Server.__init__(self)
303 self.vhosts = {}
304 end
305
306 --- Get a list of registered virtual hosts.
307 -- @return Table of virtual hosts
308 function Server.get_vhosts(self)
309 return self.vhosts
310 end
311
312 --- Register a virtual host with a given name.
313 -- @param name Hostname
314 -- @param vhost Virtual host object
315 function Server.set_vhost(self, name, vhost)
316 self.vhosts[name] = vhost
317 end
318
319 --- Send a fatal error message to given client and close the connection.
320 -- @param client Client socket
321 -- @param code HTTP status code
322 -- @param msg status message
323 function Server.error(self, client, code, msg)
324 hcode = tostring(code)
325
326 client:writeall( "HTTP/1.0 " .. hcode .. " " ..
327 statusmsg[code] .. "\r\n" )
328 client:writeall( "Connection: close\r\n" )
329 client:writeall( "Content-Type: text/plain\r\n\r\n" )
330
331 if msg then
332 client:writeall( "HTTP-Error " .. code .. ": " .. msg .. "\r\n" )
333 end
334
335 client:close()
336 end
337
338 local hdr2env = {
339 ["Content-Length"] = "CONTENT_LENGTH",
340 ["Content-Type"] = "CONTENT_TYPE",
341 ["Content-type"] = "CONTENT_TYPE",
342 ["Accept"] = "HTTP_ACCEPT",
343 ["Accept-Charset"] = "HTTP_ACCEPT_CHARSET",
344 ["Accept-Encoding"] = "HTTP_ACCEPT_ENCODING",
345 ["Accept-Language"] = "HTTP_ACCEPT_LANGUAGE",
346 ["Connection"] = "HTTP_CONNECTION",
347 ["Cookie"] = "HTTP_COOKIE",
348 ["Host"] = "HTTP_HOST",
349 ["Referer"] = "HTTP_REFERER",
350 ["User-Agent"] = "HTTP_USER_AGENT"
351 }
352
353 --- Parse the request headers and prepare the environment.
354 -- @param source line-based input source
355 -- @return Request object
356 function Server.parse_headers(self, source)
357 local env = {}
358 local req = {env = env, headers = {}}
359 local line, err
360
361 repeat -- Ignore empty lines
362 line, err = source()
363 if not line then
364 return nil, err
365 end
366 until #line > 0
367
368 env.REQUEST_METHOD, env.REQUEST_URI, env.SERVER_PROTOCOL =
369 line:match("^([A-Z]+) ([^ ]+) (HTTP/1%.[01])$")
370
371 if not env.REQUEST_METHOD then
372 return nil, "invalid magic"
373 end
374
375 local key, envkey, val
376 repeat
377 line, err = source()
378 if not line then
379 return nil, err
380 elseif #line > 0 then
381 key, val = line:match("^([%w-]+)%s?:%s?(.*)")
382 if key then
383 req.headers[key] = val
384 envkey = hdr2env[key]
385 if envkey then
386 env[envkey] = val
387 end
388 else
389 return nil, "invalid header line"
390 end
391 else
392 break
393 end
394 until false
395
396 env.SCRIPT_NAME, env.QUERY_STRING = env.REQUEST_URI:match("([^?]*)%??(.*)")
397 return req
398 end
399
400 --- Handle a new client connection.
401 -- @param client client socket
402 -- @param env superserver environment
403 function Server.process(self, client, env)
404 local sourcein = function() end
405 local sourcehdr = client:linesource()
406 local sinkout
407 local buffer
408
409 local close = false
410 local stat, code, msg, message, err
411
412 env.config.memlimit = tonumber(env.config.memlimit)
413 if env.config.memlimit and set_memory_limit then
414 set_memory_limit(env.config.memlimit)
415 end
416
417 client:setsockopt("socket", "rcvtimeo", 5)
418 client:setsockopt("socket", "sndtimeo", 5)
419
420 repeat
421 -- parse headers
422 message, err = self:parse_headers(sourcehdr)
423
424 -- any other error
425 if not message or err then
426 if err == 11 then -- EAGAIN
427 break
428 else
429 return self:error(client, 400, err)
430 end
431 end
432
433 -- Prepare sources and sinks
434 buffer = sourcehdr(true)
435 sinkout = client:sink()
436 message.server = env
437
438 if client:is_tls_socket() then
439 message.env.HTTPS = "on"
440 end
441
442 -- Addresses
443 message.env.REMOTE_ADDR = remapipv6(env.host)
444 message.env.REMOTE_PORT = env.port
445
446 local srvaddr, srvport = client:getsockname()
447 message.env.SERVER_ADDR = remapipv6(srvaddr)
448 message.env.SERVER_PORT = srvport
449
450 -- keep-alive
451 if message.env.SERVER_PROTOCOL == "HTTP/1.1" then
452 close = (message.env.HTTP_CONNECTION == "close")
453 else
454 close = not message.env.HTTP_CONNECTION
455 or message.env.HTTP_CONNECTION == "close"
456 end
457
458 -- Uncomment this to disable keep-alive
459 close = close or env.config.nokeepalive
460
461 if message.env.REQUEST_METHOD == "GET"
462 or message.env.REQUEST_METHOD == "HEAD" then
463 -- Be happy
464
465 elseif message.env.REQUEST_METHOD == "POST" then
466 -- If we have a HTTP/1.1 client and an Expect: 100-continue header
467 -- respond with HTTP 100 Continue message
468 if message.env.SERVER_PROTOCOL == "HTTP/1.1"
469 and message.headers.Expect == '100-continue' then
470 client:writeall("HTTP/1.1 100 Continue\r\n\r\n")
471 end
472
473 if message.headers['Transfer-Encoding'] and
474 message.headers['Transfer-Encoding'] ~= "identity" then
475 sourcein = chunksource(client, buffer)
476 buffer = nil
477 elseif message.env.CONTENT_LENGTH then
478 local len = tonumber(message.env.CONTENT_LENGTH)
479 if #buffer >= len then
480 sourcein = ltn12.source.string(buffer:sub(1, len))
481 buffer = buffer:sub(len+1)
482 else
483 sourcein = ltn12.source.cat(
484 ltn12.source.string(buffer),
485 client:blocksource(nil, len - #buffer)
486 )
487 end
488 else
489 return self:error(client, 411, statusmsg[411])
490 end
491
492 close = true
493 else
494 return self:error(client, 405, statusmsg[405])
495 end
496
497
498 local host = self.vhosts[message.env.HTTP_HOST] or self.vhosts[""]
499 if not host then
500 return self:error(client, 404, "No virtual host found")
501 end
502
503 local code, headers, sourceout = host:process(message, sourcein)
504 headers = headers or {}
505
506 -- Post process response
507 if sourceout then
508 if util.instanceof(sourceout, IOResource) then
509 if not headers["Content-Length"] then
510 headers["Content-Length"] = sourceout.len
511 end
512 end
513 if not headers["Content-Length"] then
514 if message.env.SERVER_PROTOCOL == "HTTP/1.1" then
515 headers["Transfer-Encoding"] = "chunked"
516 sinkout = chunksink(client)
517 else
518 close = true
519 end
520 end
521 elseif message.env.REQUEST_METHOD ~= "HEAD" then
522 headers["Content-Length"] = 0
523 end
524
525 if close then
526 headers["Connection"] = "close"
527 elseif message.env.SERVER_PROTOCOL == "HTTP/1.0" then
528 headers["Connection"] = "Keep-Alive"
529 end
530
531 headers["Date"] = date.to_http(os.time())
532 local header = {
533 message.env.SERVER_PROTOCOL .. " " .. tostring(code) .. " "
534 .. statusmsg[code],
535 "Server: LuCId-HTTPd/" .. VERSION
536 }
537
538
539 for k, v in pairs(headers) do
540 if type(v) == "table" then
541 for _, h in ipairs(v) do
542 header[#header+1] = k .. ": " .. h
543 end
544 else
545 header[#header+1] = k .. ": " .. v
546 end
547 end
548
549 header[#header+1] = ""
550 header[#header+1] = ""
551
552 -- Output
553 stat, code, msg = client:writeall(table.concat(header, "\r\n"))
554
555 if sourceout and stat then
556 if util.instanceof(sourceout, IOResource) then
557 stat, code, msg = sourceout.fd:copyz(client, sourceout.len)
558 else
559 stat, msg = ltn12.pump.all(sourceout, sinkout)
560 end
561 end
562
563
564 -- Write errors
565 if not stat then
566 if msg then
567 nixio.syslog("err", "Error sending data to " .. env.host ..
568 ": " .. msg .. "\n")
569 end
570 break
571 end
572
573 if buffer then
574 sourcehdr(buffer)
575 end
576 until close
577
578 client:shutdown()
579 client:close()
580 end