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