From b33943a6e8596c1ddfc1b771a995d3cf21e81cd6 Mon Sep 17 00:00:00 2001 From: Steven Barth Date: Sun, 30 Nov 2008 13:19:45 +0000 Subject: Merge LuCIttpd --- libs/lucittpd/luasrc/ttpd/handler/file.lua | 252 ++++++++++++++++ libs/lucittpd/luasrc/ttpd/module.lua | 121 ++++++++ libs/lucittpd/luasrc/ttpd/server.lua | 442 +++++++++++++++++++++++++++++ 3 files changed, 815 insertions(+) create mode 100644 libs/lucittpd/luasrc/ttpd/handler/file.lua create mode 100644 libs/lucittpd/luasrc/ttpd/module.lua create mode 100644 libs/lucittpd/luasrc/ttpd/server.lua (limited to 'libs/lucittpd/luasrc') diff --git a/libs/lucittpd/luasrc/ttpd/handler/file.lua b/libs/lucittpd/luasrc/ttpd/handler/file.lua new file mode 100644 index 000000000..e1f707c62 --- /dev/null +++ b/libs/lucittpd/luasrc/ttpd/handler/file.lua @@ -0,0 +1,252 @@ +--[[ + +HTTP server implementation for LuCI - file handler +(c) 2008 Steven Barth +(c) 2008 Freifunk Leipzig / Jo-Philipp Wich + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +$Id$ + +]]-- + +local ipairs, type, tonumber = ipairs, type, tonumber +local io = require "io" +local os = require "os" +local fs = require "luci.fs" +local util = require "luci.util" +local ltn12 = require "luci.ltn12" +local mod = require "luci.ttpd.module" +local srv = require "luci.ttpd.server" +local string = require "string" + +local prot = require "luci.http.protocol" +local date = require "luci.http.protocol.date" +local mime = require "luci.http.protocol.mime" +local cond = require "luci.http.protocol.conditionals" + +module "luci.ttpd.handler.file" + +Simple = util.class(mod.Handler) +Response = mod.Response + +function Simple.__init__(self, docroot, dirlist) + mod.Handler.__init__(self) + self.docroot = docroot + self.dirlist = dirlist and true or false +end + +function Simple.parse_range(self, request, size) + if not request.headers.Range then + return true + end + + local from, to = request.headers.Range:match("bytes=([0-9]*)-([0-9]*)") + if not (from or to) then + return true + end + + from, to = tonumber(from), tonumber(to) + if not (from or to) then + return true + elseif not from then + from, to = size - to, size - 1 + elseif not to then + to = size - 1 + end + + -- Not satisfiable + if from >= size then + return false + end + + -- Normalize + if to >= size then + to = size - 1 + end + + local range = "bytes " .. from .. "-" .. to .. "/" .. size + return from, (1 + to - from), range +end + +function Simple.getfile(self, uri) + local file = self.docroot .. uri:gsub("%.%./+", "") + local stat = fs.stat(file) + + return file, stat +end + +function Simple.handle_get(self, request, sourcein, sinkerr) + local file, stat = self:getfile( prot.urldecode( request.env.PATH_INFO, true ) ) + + if stat then + if stat.type == "regular" then + + -- Generate Entity Tag + local etag = cond.mk_etag( stat ) + + -- Check conditionals + local ok, code, hdrs + + ok, code, hdrs = cond.if_modified_since( request, stat ) + if ok then + ok, code, hdrs = cond.if_match( request, stat ) + if ok then + ok, code, hdrs = cond.if_unmodified_since( request, stat ) + if ok then + ok, code, hdrs = cond.if_none_match( request, stat ) + if ok then + local f, err = io.open(file) + + if f then + local code = 200 + local o, s, r = self:parse_range(request, stat.size) + + if not o then + return self:failure(416, "Invalid Range") + end + + local headers = { + ["Last-Modified"] = date.to_http( stat.mtime ), + ["Content-Type"] = mime.to_mime( file ), + ["ETag"] = etag, + ["Accept-Ranges"] = "bytes", + } + + if o == true then + o = 0 + s = stat.size + else + code = 206 + headers["Content-Range"] = r + end + + headers["Content-Length"] = s + + -- Send Response + return Response(code, headers), + srv.IOResource(f, o, s) + else + return self:failure( 403, err:gsub("^.+: ", "") ) + end + else + return Response( code, hdrs or { } ) + end + else + return Response( code, hdrs or { } ) + end + else + return Response( code, hdrs or { } ) + end + else + return Response( code, hdrs or { } ) + end + + elseif stat.type == "directory" then + + local ruri = request.request_uri:gsub("/$","") + local duri = prot.urldecode( ruri, true ) + local root = self.docroot:gsub("/$","") + + -- check for index files + local index_candidates = { + "index.html", "index.htm", "default.html", "default.htm", + "index.txt", "default.txt" + } + + -- try to find an index file and redirect to it + for i, candidate in ipairs( index_candidates ) do + local istat = fs.stat( + root .. "/" .. duri .. "/" .. candidate + ) + + if istat ~= nil and istat.type == "regular" then + return Response( 302, { + ["Location"] = ruri .. "/" .. candidate + } ) + end + end + + + local html = string.format( + '\n' .. + '\n' .. + '\n' .. + '\n' .. + 'Index of %s/\n' .. + '

Index of %s/


    ', + duri, duri + ) + + local entries = fs.dir( file ) + + if type(entries) == "table" then + for i, e in util.spairs( + entries, function(a,b) + if entries[a] == '..' then + return true + elseif entries[b] == '..' then + return false + else + return ( entries[a] < entries[b] ) + end + end + ) do + if e ~= '.' and ( e == '..' or e:sub(1,1) ~= '.' ) then + local estat = fs.stat( file .. "/" .. e ) + + if estat.type == "directory" then + html = html .. string.format( + '
  • %s/ ' .. + '(directory)
    ' .. + 'Changed: %s

  • ', + ruri, prot.urlencode( e ), e, + date.to_http( estat.mtime ) + ) + else + html = html .. string.format( + '
  • %s ' .. + '(%s)
    ' .. + 'Size: %i Bytes | ' .. + 'Changed: %s

  • ', + ruri, prot.urlencode( e ), e, + mime.to_mime( e ), + estat.size, date.to_http( estat.mtime ) + ) + end + end + end + + html = html .. '

' + + return Response( + 200, { + ["Date"] = date.to_http( os.time() ); + ["Content-Type"] = "text/html; charset=ISO-8859-15"; + } + ), ltn12.source.string(html) + else + return self:failure(403, "Permission denied") + end + else + return self:failure(403, "Unable to transmit " .. stat.type .. " " .. file) + end + else + return self:failure(404, "No such file: " .. file) + end +end + +function Simple.handle_head(self, ...) + return (self:handle_get(...)) +end diff --git a/libs/lucittpd/luasrc/ttpd/module.lua b/libs/lucittpd/luasrc/ttpd/module.lua new file mode 100644 index 000000000..1a7c57473 --- /dev/null +++ b/libs/lucittpd/luasrc/ttpd/module.lua @@ -0,0 +1,121 @@ +--[[ +LuCI - Lua Configuration Interface + +Copyright 2008 Steven Barth + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +$Id$ +]]-- + +local pcall, ipairs, tonumber, type, next = pcall, ipairs, tonumber, type, next +local util = require "luci.util" +local http = require "luci.http.protocol" +local ltn12 = require "luci.ltn12" +local table = require "table" + +module "luci.ttpd.module" + + +-- Server handler implementation +Handler = util.class() + +-- Constructor +function Handler.__init__(self) + self.handler = {} + self.filters = {} + self.modifiers = {} +end + +-- Add a filter +function Handler.setfilter(self, filter, key) + self.filters[(key) or (#self.filters+1)] = filter +end + +-- Add a modifier +function Handler.setmodifier(self, modifier, key) + self.modifiers[(pos) or (#self.modifiers+1)] = modifier +end + +-- Creates a failure reply +function Handler.failure(self, code, message) + local response = Response(code, { ["Content-Type"] = "text/plain" }) + local sourceout = ltn12.source.string(message) + + return response, sourceout +end + +-- Processes a request +function Handler.process(self, request, sourcein, sinkerr) + local stat, response, sourceout + + -- Detect request Method + local hname = "handle_" .. request.request_method + if self[hname] then + local t = { + processor = self[hname], + handler = self, + request = request, + sourcein = sourcein, + sinkerr = sinkerr + } + + if next(self.modifiers) then + for _, mod in util.kspairs(self.modifiers) do + mod(t) + end + end + + -- Run the handler + stat, response, sourceout = pcall( + t.processor, t.handler, t.request, t.sourcein, t.sinkerr + ) + + -- Check for any errors + if not stat then + response, sourceout = self:failure(500, response) + elseif next(self.filters) then + local t = { + response = response, + sourceout = sourceout, + sinkerr = t.sinkerr + } + + for _, filter in util.kspairs(self.filters) do + filter(t) + end + + response = t.response + sourceout = t.sourceout + end + else + response, sourceout = self:failure(405, http.protocol.statusmsg[405]) + end + + -- Check data + if not util.instanceof(response, Response) then + response, sourceout = self:failure(500, "Core error: Invalid module response!") + end + + return response, sourceout +end + +-- Handler Response +Response = util.class() + +function Response.__init__(self, status, headers) + self.status = tonumber(status) or 200 + self.headers = (type(headers) == "table") and headers or {} +end + +function Response.addheader(self, key, value) + self.headers[key] = value +end + +function Response.setstatus(self, status) + self.status = status +end \ No newline at end of file diff --git a/libs/lucittpd/luasrc/ttpd/server.lua b/libs/lucittpd/luasrc/ttpd/server.lua new file mode 100644 index 000000000..4cb246af8 --- /dev/null +++ b/libs/lucittpd/luasrc/ttpd/server.lua @@ -0,0 +1,442 @@ +--[[ +LuCIttpd +(c) 2008 Steven Barth +(c) 2008 Jo-Philipp Wich + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +$Id$ +]]-- + +local ipairs, pairs = ipairs, pairs +local tostring, tonumber = tostring, tonumber +local pcall, assert = pcall, assert + +local os = require "os" +local io = require "io" +local util = require "luci.util" +local ltn12 = require "luci.ltn12" +local proto = require "luci.http.protocol" +local string = require "string" +local date = require "luci.http.protocol.date" + +module "luci.ttpd.server" + +BUFSIZE = 4096 +VERSION = 0.91 + + +-- File Resource +IOResource = util.class() + +function IOResource.__init__(self, fd, offset, len) + self.fd, self.offset, self.len = fd, offset, len +end + + +VHost = util.class() + +function VHost.__init__(self, handler) + self.handler = handler + self.dhandler = {} +end + +function VHost.process(self, request, sourcein, sinkerr, ...) + local handler = self.handler + + local uri = request.env.REQUEST_URI:match("^([^?]*)") + + -- SCRIPT_NAME + request.env.SCRIPT_NAME = "" + + -- Call URI part + request.env.PATH_INFO = uri + + for k, dhandler in pairs(self.dhandler) do + if k == uri or k.."/" == uri:sub(1, #k+1) then + handler = dhandler + request.env.SCRIPT_NAME = k + request.env.PATH_INFO = uri:sub(#k+1) + break; + end + end + + if handler then + return handler:process(request, sourcein, sinkerr, ...) + end +end + +function VHost.get_default_handler(self) + return self.handler +end + +function VHost.set_default_handler(self, handler) + self.handler = handler +end + +function VHost.get_handlers(self) + return self.dhandler +end + +function VHost.set_handler(self, match, handler) + self.dhandler[match] = handler +end + + + +Server = util.class() + +function Server.__init__(self, host) + self.host = host + self.vhosts = {} + + self.rbuf = "" + self.wbuf = "" +end + +function Server.get_default_vhost(self) + return self.host +end + +function Server.set_default_vhost(self, vhost) + self.host = vhost +end + +function Server.get_vhosts(self) + return self.vhosts +end + +function Server.set_vhost(self, name, vhost) + self.vhosts[name] = vhost +end + +function Server.flush(self) + if #self.wbuf > 0 then + self._write(self.wbuf) + self.wbuf = "" + end +end + +function Server.read(self, len) + while #self.rbuf < len do + self.rbuf = self.rbuf .. self._read(len - #self.rbuf) + end + + local chunk = self.rbuf:sub(1, len) + self.rbuf = self.rbuf:sub(len + 1) + return chunk +end + +function Server.limitsource(self, limit) + limit = limit or 0 + + return function() + if limit < 1 then + return nil + else + local read = (limit > BUFSIZE) and BUFSIZE or limit + limit = limit - read + return self:read(read) + end + end +end + +-- Adapted from Luaposix +function Server.receiveheaders(self) + local line, name, value, err + local headers = {} + -- get first line + line, err = self:readline() + if err then return nil, err end + -- headers go until a blank line is found + while line do + -- get field-name and value + _, _, name, value = line:find("^(.-):%s*(.*)") + if not (name and value) then return nil, "malformed reponse headers" end + name = name:lower() + -- get next line (value might be folded) + line, err = self:readline() + if err then return nil, err end + -- unfold any folded values + while line:find("^%s") do + value = value .. line + line = self:readline() + if err then return nil, err end + end + -- save pair in table + if headers[name] then headers[name] = headers[name] .. ", " .. value + else headers[name] = value end + end + return headers +end + +function Server.readchunk(self) + -- get chunk size, skip extention + local line, err = self:readline() + if err then return nil, err end + local size = tonumber(line:gsub(";.*", ""), 16) + if not size then return nil, "invalid chunk size" end + -- was it the last chunk? + if size > 0 then + -- if not, get chunk and skip terminating CRLF + local chunk, err, part = self:read(size) + if chunk then self:readline() end + return chunk, err + else + -- if it was, read trailers into headers table + headers, err = self:receiveheaders() + if not headers then return nil, err end + end +end + +function Server.readline(self) + if #self.rbuf < 1 then + self.rbuf = self._read(BUFSIZE) + end + + while true do + local le = self.rbuf:find("\r\n", nil, true) + if le then + if le == 1 then -- EoH + self.rbuf = self.rbuf:sub(le + 2) + return nil + else -- Header + local line = self.rbuf:sub(1, le - 1) + self.rbuf = self.rbuf:sub(le + 2) + return line + end + else + if #self.rbuf >= BUFSIZE then + return nil, "Invalid Request" + end + self.rbuf = self.rbuf .. self._read(BUFSIZE-#self.rbuf) + end + end +end + +function Server.sink(self) + return function(chunk, err) + if err then + return nil, err + elseif chunk then + local stat, err = pcall(self.write, self, chunk) + if stat then + return stat + else + return nil, err + end + else + return true + end + end +end + +function Server.chunksink(self) + return function(chunk, err) + local stat, err = pcall(self.writechunk, self, chunk) + if stat then + return stat + else + return nil, err + end + end +end + +function Server.writechunk(self, chunk, err) + self:flush() + if not chunk then return self._write("0\r\n\r\n") end + local size = string.format("%X\r\n", #chunk) + return self._write(size .. chunk .. "\r\n") +end + +function Server.write(self, chunk) + while #chunk > 0 do + local missing = BUFSIZE - #self.wbuf + self.wbuf = self.wbuf .. chunk:sub(1, missing) + chunk = chunk:sub(missing + 1) + if #self.wbuf == BUFSIZE then + assert(self._write(self.wbuf)) + self.wbuf = "" + end + end +end + +function Server.close(self) + self:flush() + self._close() +end + +function Server.sendfile(self, fd, offset, len) + self:flush() + self._sendfile(fd, offset, len) +end + + +function Server.error(self, code, msg) + hcode = tostring(code) + + self:write( "HTTP/1.0 " .. hcode .. " " .. + proto.statusmsg[code] .. "\r\n" ) + self:write( "Connection: close\r\n" ) + self:write( "Content-Type: text/plain\r\n\r\n" ) + + if msg then + self:write( "HTTP-Error " .. code .. ": " .. msg .. "\r\n" ) + end +end + + +function Server.process(self, functions) + util.update(self, functions) + + local sourcein = ltn12.source.empty() + local sourcehdr = function() return self:readline() or "" end + local sinkerr = ltn12.sink.file( io.stderr ) + local sinkout = self:sink() + + local close = false + local stat, message, err + + repeat + -- parse headers + stat, message, err = pcall(proto.parse_message_header, sourcehdr) + + -- remote socket closed + if not stat and message == 0 then + break + end + + -- remote timeout + if not stat and message == 11 then + --self:error(408) + break + end + + -- any other error + if not stat or not message then + self:error(400, err) + break + end + + -- keep-alive + if message.http_version == 1.1 then + close = (message.env.HTTP_CONNECTION == "close") + else + close = not message.env.HTTP_CONNECTION or message.env.HTTP_CONNECTION == "close" + end + -- Uncomment this to disable keep-alive + -- close = true + + if message.request_method == "get" or message.request_method == "head" then + -- Be happy + + elseif message.request_method == "post" then + -- If we have a HTTP/1.1 client and an Expect: 100-continue header then + -- respond with HTTP 100 Continue message + if message.http_version == 1.1 and message.headers['Expect'] and + message.headers['Expect'] == '100-continue' + then + self:write("HTTP/1.1 100 Continue\r\n\r\n") + end + + if message.headers['Transfer-Encoding'] and + message.headers['Transfer-Encoding'] ~= "identity" then + sourcein = function() return self:readchunk() end + elseif message.env.CONTENT_LENGTH then + sourcein = self:limitsource( + tonumber(message.env.CONTENT_LENGTH) + ) + else + self:error( 411, proto.statusmsg[411] ) + break + end + else + self:error( 405, proto.statusmsg[405] ) + break + + end + + + local host = self.vhosts[message.env.HTTP_HOST] or self.host + if not host then + self:error( 500, "Unable to find matching host" ) + break; + end + + local response, sourceout = host:process( + message, sourcein, sinkerr, + client, io.stderr + ) + if not response then + self:error( 500, "Error processing handler" ) + end + + -- Post process response + if sourceout then + if util.instanceof(sourceout, IOResource) then + if not response.headers["Content-Length"] then + response.headers["Content-Length"] = sourceout.len + end + end + if not response.headers["Content-Length"] then + if message.http_version == 1.1 then + response.headers["Transfer-Encoding"] = "chunked" + sinkout = self:chunksink() + else + close = true + end + end + elseif message.request_method ~= "head" then + response.headers["Content-Length"] = 0 + end + + if close then + response.headers["Connection"] = "close" + end + + response.headers["Date"] = date.to_http(os.time()) + + local header = + message.env.SERVER_PROTOCOL .. " " .. + tostring(response.status) .. " " .. + proto.statusmsg[response.status] .. "\r\n" + + header = header .. "Server: LuCIttpd/" .. tostring(VERSION) .. "\r\n" + + + for k,v in pairs(response.headers) do + header = header .. k .. ": " .. v .. "\r\n" + end + + -- Output + local stat, err = pcall(function() + self:write(header .. "\r\n") + + if sourceout then + if util.instanceof(sourceout, IOResource) then + self:sendfile(sourceout.fd, sourceout.offset, sourceout.len) + else + ltn12.pump.all(sourceout, sinkout) + end + end + + self:flush() + end) + + -- Write errors + if not stat then + if err == 107 then + -- Remote end closed the socket, so do we + elseif err then + io.stderr:write("Error sending data: " .. err .. "\n") + end + break + end + until close + + self:close() +end -- cgit v1.2.3