0fe947316635251c6f207c6c793514bd92324335
[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, tostring(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"] and not close 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                                 if not headers["Transfer-Encoding"] then
558                                         stat, code, msg = sourceout.fd:copyz(client, sourceout.len)
559                                         sourceout = nil
560                                 else
561                                         sourceout = sourceout.fd:blocksource(nil, sourceout.len)
562                                 end
563                         end
564
565                         if sourceout then
566                                 stat, msg = ltn12.pump.all(sourceout, sinkout)
567                         end
568                 end
569
570
571                 -- Write errors
572                 if not stat then
573                         if msg then
574                                 nixio.syslog("err", "Error sending data to " .. env.host ..
575                                         ": " .. msg .. "\n")
576                         end
577                         break
578                 end
579                 
580                 if buffer then
581                         sourcehdr(buffer)
582                 end
583         until close
584         
585         client:shutdown()
586         client:close()
587 end