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