root/luci/trunk/libs/lucid-http/luasrc/lucid/http/server.lua @ 5157

Revision 5157, 14.2 KB (checked in by Cyrus, 4 years ago)

Add native support for homepages

Line 
1--[[
2LuCId HTTP-Slave
3(c) 2009 Steven Barth <steven@midlink.org>
4
5Licensed under the Apache License, Version 2.0 (the "License");
6you may not use this file except in compliance with the License.
7You may obtain a copy of the License at
8
9        http://www.apache.org/licenses/LICENSE-2.0
10
11$Id$
12]]--
13
14local ipairs, pairs = ipairs, pairs
15local tostring, tonumber = tostring, tonumber
16local pcall, assert, type = pcall, assert, type
17local set_memory_limit = set_memory_limit
18
19local os = require "os"
20local nixio = require "nixio"
21local util = require "luci.util"
22local ltn12 = require "luci.ltn12"
23local proto = require "luci.http.protocol"
24local table = require "table"
25local date = require "luci.http.protocol.date"
26
27--- HTTP Daemon
28-- @cstyle instance
29module "luci.lucid.http.server"
30
31VERSION = "1.0"
32
33statusmsg = {
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
57IOResource = util.class()
58
59function IOResource.__init__(self, fd, len)
60    self.fd, self.len = fd, len
61end
62
63
64--- Create a server handler.
65-- @class function
66-- @param name Name
67-- @return Handler
68Handler = util.class()
69
70function Handler.__init__(self, name)
71    self.name = name or tostring(self)
72end
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
78function Handler.failure(self, code, msg)   
79    return code, { ["Content-Type"] = "text/plain" }, ltn12.source.string(msg)
80end
81
82--- Add an access restriction.
83-- @param restriction Restriction specification
84function 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
90end
91
92--- Enforce access restrictions.
93-- @param request Request object
94-- @return nil or HTTP statuscode, table of headers, response source
95function 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")
144end
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
150function 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
173end
174
175
176--- Create a Virtual Host.
177-- @class function
178-- @return Virtual Host
179VHost = util.class()
180
181function VHost.__init__(self)
182    self.handlers = {}
183end
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
189function 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
221end
222
223--- Get a list of registered handlers.
224-- @return Table of handlers
225function VHost.get_handlers(self)
226    return self.handlers
227end
228
229--- Register handler with a given URI prefix.
230-- @oaram match URI prefix
231-- @param handler Handler object
232function VHost.set_handler(self, match, handler)
233    self.handlers[match] = handler
234end
235
236-- Remap IPv6-IPv4-compatibility addresses back to IPv4 addresses.
237local 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 
244end
245
246-- Create a source that decodes chunked-encoded data from a socket.
247local 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
288end
289
290-- Create a sink that chunk-encodes data and writes it on a given socket.
291local 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
299end
300
301
302--- Create a server object.
303-- @class function
304-- @return Server object
305Server = util.class()
306
307function Server.__init__(self)
308    self.vhosts = {}
309end
310
311--- Get a list of registered virtual hosts.
312-- @return Table of virtual hosts
313function Server.get_vhosts(self)
314    return self.vhosts
315end
316
317--- Register a virtual host with a given name.
318-- @param name Hostname
319-- @param vhost Virtual host object
320function Server.set_vhost(self, name, vhost)
321    self.vhosts[name] = vhost
322end
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
328function 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()
341end
342
343local 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
361function 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
403end
404
405--- Handle a new client connection.
406-- @param client client socket
407-- @param env superserver environment
408function 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        elseif message.env.SERVER_PROTOCOL == "HTTP/1.0" then
533            headers["Connection"] = "Keep-Alive"
534        end 
535
536        headers["Date"] = date.to_http(os.time())
537        local header = {
538            message.env.SERVER_PROTOCOL .. " " .. tostring(code) .. " " 
539                .. statusmsg[code],
540            "Server: LuCId-HTTPd/" .. VERSION
541        }
542
543       
544        for k, v in pairs(headers) do
545            if type(v) == "table" then
546                for _, h in ipairs(v) do
547                    header[#header+1] = k .. ": " .. h
548                end
549            else
550                header[#header+1] = k .. ": " .. v
551            end
552        end
553
554        header[#header+1] = ""
555        header[#header+1] = ""
556       
557        -- Output
558        stat, code, msg = client:writeall(table.concat(header, "\r\n"))
559
560        if sourceout and stat then
561            if util.instanceof(sourceout, IOResource) then
562                if not headers["Transfer-Encoding"] then
563                    stat, code, msg = sourceout.fd:copyz(client, sourceout.len)
564                    sourceout = nil
565                else
566                    sourceout = sourceout.fd:blocksource(nil, sourceout.len)
567                end
568            end
569
570            if sourceout then
571                stat, msg = ltn12.pump.all(sourceout, sinkout)
572            end
573        end
574
575
576        -- Write errors
577        if not stat then
578            if msg then
579                nixio.syslog("err", "Error sending data to " .. env.host ..
580                    ": " .. msg .. "\n")
581            end
582            break
583        end
584       
585        if buffer then
586            sourcehdr(buffer)
587        end
588    until close
589   
590    client:shutdown()
591    client:close()
592end
Note: See TracBrowser for help on using the browser.