-----------------------------------------------------------------------------
-- Xavante HTTP handler
--
-- Authors: Javier Guerra and Andre Carregal
-- Copyright (c) 2004-2007 Kepler Project
--
-- $Id: httpd.lua,v 1.45 2009/08/10 20:00:59 mascarenhas Exp $
-----------------------------------------------------------------------------
local string = require "string"
local table = require "table"
local os = require "os"
local io = require "io"
local socket = require "socket"
local url = require "socket.url"
local copas = require "copas"
local unpack = table.unpack or unpack
local _M = {}
local _serversoftware = ""
local _serverports = {}
function _M.strsplit (str)
local words = {}
for w in string.gmatch (str, "%S+") do
table.insert (words, w)
end
return words
end
-- Manages one connection, maybe several requests
-- params:
-- skt : client socket
function _M.connection (skt)
copas.setErrorHandler (_M.errorhandler)
skt:setoption ("tcp-nodelay", true)
local srv, port = skt:getsockname ()
local req = {
rawskt = skt,
srv = srv,
port = port,
copasskt = copas.wrap (skt),
}
req.socket = req.copasskt
req.serversoftware = _serversoftware
while _M.read_method (req) do
local res
_M.read_headers (req)
repeat
req.params = nil
_M.parse_url (req)
res = _M.make_response (req)
until _M.handle_request (req, res) ~= "reparse"
_M.send_response (req, res)
req.socket:flush ()
if not res.keep_alive then
break
end
end
end
function _M.errorhandler (msg, co, skt)
msg = tostring(msg)
io.stderr:write(" Xavante Error: "..msg.."\n", " "..tostring(co).."\n", " "..tostring(skt).."\n")
skt:send ("HTTP/1.0 200 OK\r\n")
skt:send (string.format ("Date: %s\r\n\r\n", os.date ("!%a, %d %b %Y %H:%M:%S GMT")))
skt:send (string.format ([[
Xavante Error!
Xavante Error!
%s
]], string.gsub (msg, "\n", "
\n")))
end
-- gets and parses the request line
-- params:
-- req: request object
-- returns:
-- true if ok
-- false if connection closed
-- sets:
-- req.cmd_mth: http method
-- req.cmd_url: url requested (as sent by the client)
-- req.cmd_version: http version (usually 'HTTP/1.1')
function _M.read_method (req)
local err
req.cmdline, err = req.socket:receive ()
if not req.cmdline then return nil end
req.cmd_mth, req.cmd_url, req.cmd_version = unpack (_M.strsplit (req.cmdline))
req.cmd_mth = string.upper (req.cmd_mth or 'GET')
req.cmd_url = req.cmd_url or '/'
return true
end
-- gets and parses the request header fields
-- params:
-- req: request object
-- sets:
-- req.headers: table of header fields, as name => value
function _M.read_headers (req)
local headers = {}
local prevval, prevname
while 1 do
local l,err = req.socket:receive ()
if (not l or l == "") then
req.headers = headers
return
end
local _,_, name, value = string.find (l, "^([^: ]+)%s*:%s*(.+)")
name = string.lower (name or '')
if name then
prevval = headers [name]
if prevval then
value = prevval .. "," .. value
end
headers [name] = value
prevname = name
elseif prevname then
headers [prevname] = headers [prevname] .. l
end
end
end
function _M.parse_url (req)
local def_url = string.format ("http://%s%s", req.headers.host or "", req.cmd_url or "")
req.parsed_url = url.parse (def_url or '')
req.parsed_url.port = req.parsed_url.port or req.port
req.built_url = url.build (req.parsed_url)
req.relpath = url.unescape (req.parsed_url.path)
end
-- sets the default response headers
function _M.default_headers (req)
return {
Date = os.date ("!%a, %d %b %Y %H:%M:%S GMT"),
Server = _serversoftware,
}
end
function _M.add_res_header (res, h, v)
if string.lower(h) == "status" then
res.statusline = "HTTP/1.1 "..v
else
local prevval = res.headers [h]
if (prevval == nil) then
res.headers[h] = v
elseif type (prevval) == "table" then
table.insert (prevval, v)
else
res.headers[h] = {prevval, v}
end
end
end
-- sends the response headers
-- params:
-- res: response object
-- uses:
-- res.sent_headers : if true, headers are already sent, does nothing
-- res.statusline : response status, if nil, sends 200 OK
-- res.headers : table of header fields to send
local function send_res_headers (res)
if (res.sent_headers) then
return
end
if package.loaded["xavante.cookies"] then
local cookies = require "xavante.cookies"
cookies.set_res_cookies (res)
end
res.statusline = res.statusline or "HTTP/1.1 200 OK"
res.socket:send (res.statusline.."\r\n")
for name, value in pairs (res.headers) do
if type(value) == "table" then
for _, value in ipairs(value) do
res.socket:send (string.format ("%s: %s\r\n", name, value))
end
else
res.socket:send (string.format ("%s: %s\r\n", name, value))
end
end
res.socket:send ("\r\n")
res.sent_headers = true;
end
-- sends content directly to client
-- sends headers first, if necesary
-- params:
-- res ; response object
-- data : content data to send
local function send_res_data (res, data)
if not res.sent_headers then
send_res_headers (res)
end
if not data or data == "" then
return
end
if data then
if res.chunked then
res.socket:send (string.format ("%X\r\n", string.len (data)))
res.socket:send (data)
res.socket:send ("\r\n")
else
res.socket:send (data)
end
end
end
function _M.make_response (req)
local res = {
req = req,
socket = req.socket,
headers = _M.default_headers (req),
add_header = _M.add_res_header,
send_headers = send_res_headers,
send_data = send_res_data,
}
return res
end
-- sends prebuilt content to the client
-- if possible, sets Content-Length: header field
-- params:
-- req : request object
-- res : response object
-- uses:
-- res.content : content data to send
-- sets:
-- res.keep_alive : if possible to keep using the same connection
function _M.send_response (req, res)
if res.content then
if not res.sent_headers then
if (type (res.content) == "table" and not res.chunked) then
res.content = table.concat (res.content)
end
if type (res.content) == "string" then
res.headers["Content-Length"] = string.len (res.content)
end
end
else
if not res.sent_headers then
res.statusline = "HTTP/1.1 204 No Content"
res.headers["Content-Length"] = 0
end
end
if res.chunked then
res:add_header ("Transfer-Encoding", "chunked")
end
if res.chunked or ((res.headers ["Content-Length"]) and req.headers ["connection"] == "Keep-Alive")
then
res.headers ["Connection"] = "Keep-Alive"
res.keep_alive = true
else
res.keep_alive = nil
end
if res.content then
if type (res.content) == "table" then
for _,v in ipairs (res.content) do res:send_data (v) end
else
res:send_data (res.content)
end
else
res:send_headers ()
end
if res.chunked then
res.socket:send ("0\r\n\r\n")
end
end
function _M.getparams (req)
if not req.parsed_url.query then return nil end
if req.params then return req.params end
local params = {}
req.params = params
for parm in string.gmatch (req.parsed_url.query, "([^&]+)") do
local k,v = string.match (parm, "(.*)=(.*)")
k = url.unescape (k)
v = url.unescape (v)
if k ~= nil then
if params[k] == nil then
params[k] = v
elseif type (params[k]) == "table" then
table.insert (params[k], v)
else
params[k] = {params[k], v}
end
end
end
return params
end
function _M.err_404 (req, res)
res.statusline = "HTTP/1.1 404 Not Found"
res.headers ["Content-Type"] = "text/html"
res.content = string.format ([[
404 Not Found
Not Found
The requested URL %s was not found on this server.
]], req.built_url);
return res
end
function _M.err_403 (req, res)
res.statusline = "HTTP/1.1 403 Forbidden"
res.headers ["Content-Type"] = "text/html"
res.content = string.format ([[
403 Forbidden
Forbidden
You are not allowed to access the requested URL %s .
]], req.built_url);
return res
end
function _M.err_405 (req, res)
res.statusline = "HTTP/1.1 405 Method Not Allowed"
res.content = string.format ([[
405 Method Not Allowed
Not Found
The Method %s is not allowed for URL %s on this server.
]], req.cmd_mth, req.built_url);
return res
end
function _M.redirect (res, d)
res.headers ["Location"] = d
res.statusline = "HTTP/1.1 302 Found"
res.content = "redirect"
end
function _M.register (host, port, serversoftware)
local _server = assert(socket.bind(host, port))
_serversoftware = serversoftware
local _ip, _port = _server:getsockname()
_serverports[_port] = true
copas.addserver(_server, _M.connection)
end
function _M.get_ports()
local ports = {}
for k, _ in pairs(_serverports) do
table.insert(ports, tostring(k))
end
return ports
end
return _M