build: split into luci and luci-addons packages
[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