-- Prosody IM
-- Copyright (C) 2008-2009 Matthew Wild
-- Copyright (C) 2008-2009 Waqas Hussain
--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
local socket = require " socket "
local server = require " net.server "
local url_parse = require " socket.url " . parse ;
local connlisteners_start = require " net.connlisteners " . start ;
local connlisteners_get = require " net.connlisteners " . get ;
local listener ;
local t_insert , t_concat = table.insert , table.concat ;
local s_match , s_gmatch = string.match , string.gmatch ;
local tonumber , tostring , pairs , ipairs , type = tonumber , tostring , pairs , ipairs , type ;
local urlencode = function ( s ) return s and ( s : gsub ( " %W " , function ( c ) return string.format ( " %%%02x " , c : byte ( ) ) ; end ) ) ; end
local log = require " util.logger " . init ( " httpserver " ) ;
local http_servers = { } ;
module " httpserver "
local default_handler ;
local function expectbody ( reqt )
return reqt.method == " POST " ;
end
local function send_response ( request , response )
-- Write status line
local resp ;
if response.body then
local body = tostring ( response.body ) ;
log ( " debug " , " Sending response to %s " , request.id ) ;
resp = { " HTTP/1.0 " , response.status or " 200 OK " , " \r \n " } ;
local h = response.headers ;
if h then
for k , v in pairs ( h ) do
t_insert ( resp , k ) ;
t_insert ( resp , " : " ) ;
t_insert ( resp , v ) ;
t_insert ( resp , " \r \n " ) ;
end
end
if not ( h and h [ " Content-Length " ] ) then
t_insert ( resp , " Content-Length: " ) ;
t_insert ( resp , # body ) ;
t_insert ( resp , " \r \n " ) ;
end
t_insert ( resp , " \r \n " ) ;
if request.method ~= " HEAD " then
t_insert ( resp , body ) ;
end
else
-- Response we have is just a string (the body)
log ( " debug " , " Sending 200 response to %s " , request.id or " <none> " ) ;
resp = { " HTTP/1.0 200 OK \r \n " } ;
t_insert ( resp , " Connection: close \r \n " ) ;
t_insert ( resp , " Content-Length: " ) ;
t_insert ( resp , # response ) ;
t_insert ( resp , " \r \n \r \n " ) ;
t_insert ( resp , response ) ;
end
request.write ( t_concat ( resp ) ) ;
if not request.stayopen then
request : destroy ( ) ;
end
end
local function call_callback ( request , err )
if request.handled then return ; end
request.handled = true ;
local callback = request.callback ;
if not callback and request.path then
local path = request.url . path ;
local base = path : match ( " ^/([^/?]+) " ) ;
if not base then
base = path : match ( " ^http://[^/?]+/([^/?]+) " ) ;
end
callback = ( request.server and request.server . handlers [ base ] ) or default_handler ;
end
if callback then
if err then
log ( " debug " , " Request error: " .. err ) ;
if not callback ( nil , err , request ) then
destroy_request ( request ) ;
end
return ;
end
local response = callback ( request.method , request.body and t_concat ( request.body ) , request ) ;
if response then
if response == true and not request.destroyed then
-- Keep connection open, we will reply later
log ( " debug " , " Request %s left open, on_destroy is %s " , request.id , tostring ( request.on_destroy ) ) ;
elseif response ~= true then
-- Assume response
send_response ( request , response ) ;
destroy_request ( request ) ;
end
else
log ( " debug " , " Request handler provided no response, destroying request... " ) ;
-- No response, close connection
destroy_request ( request ) ;
end
end
end
local function request_reader ( request , data , startpos )
if not data then
if request.body then
call_callback ( request ) ;
else
-- Error.. connection was closed prematurely
call_callback ( request , " connection-closed " ) ;
end
-- Here we force a destroy... the connection is gone, so we can't reply later
destroy_request ( request ) ;
return ;
end
if request.state == " body " then
log ( " debug " , " Reading body... " )
if not request.body then request.body = { } ; request.havebodylength , request.bodylength = 0 , tonumber ( request.headers [ " content-length " ] ) ; end
if startpos then
data = data : sub ( startpos , - 1 )
end
t_insert ( request.body , data ) ;
if request.bodylength then
request.havebodylength = request.havebodylength + # data ;
if request.havebodylength >= request.bodylength then
-- We have the body
call_callback ( request ) ;
end
end
elseif request.state == " headers " then
log ( " debug " , " Reading headers... " )
local pos = startpos ;
local headers = request.headers or { } ;
for line in data : gmatch ( " (.-) \r \n " ) do
startpos = ( startpos or 1 ) + # line + 2 ;
local k , v = line : match ( " (%S+): (.+) " ) ;
if k and v then
headers [ k : lower ( ) ] = v ;
-- log("debug", "Header: "..k:lower().." = "..v);
elseif # line == 0 then
request.headers = headers ;
break ;
else
log ( " debug " , " Unhandled header line: " .. line ) ;
end
end
if not expectbody ( request ) then
call_callback ( request ) ;
return ;
end
-- Reached the end of the headers
request.state = " body " ;
if # data > startpos then
return request_reader ( request , data : sub ( startpos , - 1 ) ) ;
end
elseif request.state == " request " then
log ( " debug " , " Reading request line... " )
local method , path , http , linelen = data : match ( " ^(%S+) (%S+) HTTP/(%S+) \r \n () " , startpos ) ;
if not method then
return call_callback ( request , " invalid-status-line " ) ;
end
request.method , request.path , request.httpversion = method , path , http ;
request.url = url_parse ( request.path ) ;
log ( " debug " , method .. " request for " .. tostring ( request.path ) .. " on port " .. request.handler . serverport ( ) ) ;
if request.onlystatus then
if not call_callback ( request ) then
return ;
end
end
request.state = " headers " ;
if # data > linelen then
return request_reader ( request , data : sub ( linelen , - 1 ) ) ;
end
end
end
-- The default handler for requests
default_handler = function ( method , body , request )
log ( " debug " , method .. " request for " .. tostring ( request.path ) .. " on port " .. request.handler . serverport ( ) ) ;
return { status = " 404 Not Found " ,
headers = { [ " Content-Type " ] = " text/html " } ,
body = " <html><head><title>Page Not Found</title></head><body>Not here :(</body></html> " } ;
end
function new_request ( handler )
return { handler = handler , conn = handler.socket ,
write = handler.write , state = " request " ,
server = http_servers [ handler.serverport ( ) ] ,
send = send_response ,
destroy = destroy_request ,
id = tostring { } : match ( " %x+$ " )
} ;
end
function destroy_request ( request )
log ( " debug " , " Destroying request %s " , request.id ) ;
listener = listener or connlisteners_get ( " httpserver " ) ;
if not request.destroyed then
request.destroyed = true ;
if request.on_destroy then
log ( " debug " , " Request has destroy callback " ) ;
request.on_destroy ( request ) ;
else
log ( " debug " , " Request has no destroy callback " ) ;
end
request.handler . close ( )
if request.conn then
listener.disconnect ( request.conn , " closed " ) ;
end
end
end
function new ( params )
local http_server = http_servers [ params.port ] ;
if not http_server then
http_server = { handlers = { } } ;
http_servers [ params.port ] = http_server ;
-- We weren't already listening on this port, so start now
connlisteners_start ( " httpserver " , params ) ;
end
if params.base then
http_server.handlers [ params.base ] = params.handler ;
end
end
function set_default_handler ( handler )
default_handler = handler ;
end
function new_from_config ( ports , handle_request , default_options )
if type ( handle_request ) == " string " then -- COMPAT with old plugins
log ( " warn " , " Old syntax of httpserver.new_from_config being used to register %s " , handle_request ) ;
handle_request , default_options = default_options , { base = handle_request } ;
end
for _ , options in ipairs ( ports ) do
local port = default_options.port or 5280 ;
local base = default_options.base ;
local ssl = default_options.ssl or false ;
local interface = default_options.interface ;
if type ( options ) == " number " then
port = options ;
elseif type ( options ) == " table " then
port = options.port or port ;
base = options.path or base ;
ssl = options.ssl or ssl ;
interface = options.interface or interface ;
elseif type ( options ) == " string " then
base = options ;
end
if ssl then
ssl.mode = " server " ;
ssl.protocol = " sslv23 " ;
end
new { port = port , interface = interface ,
base = base , handler = handle_request ,
ssl = ssl , type = ( ssl and " ssl " ) or " tcp " } ;
end
end
_M.request_reader = request_reader ;
_M.send_response = send_response ;
_M.urlencode = urlencode ;
return _M ;