GSoC: Documentation #2
[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