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