-- Prosody IM
-- Copyright (C) 2008-2010 Matthew Wild
-- Copyright (C) 2008-2010 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 httpstream_new = require " util.httpstream " . new ;
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 or response.headers then
local body = response.body and 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 .. " : " .. v .. " \r \n " ) ;
end
end
if body and not ( h and h [ " Content-Length " ] ) then
t_insert ( resp , " Content-Length: " .. # body .. " \r \n " ) ;
end
t_insert ( resp , " \r \n " ) ;
if body and request.method ~= " HEAD " then
t_insert ( resp , body ) ;
end
request.write ( t_concat ( resp ) ) ;
else
-- Response we have is just a string (the body)
log ( " debug " , " Sending 200 response to %s " , request.id or " <none> " ) ;
local resp = " HTTP/1.0 200 OK \r \n "
.. " Connection: close \r \n "
.. " Content-Type: text/html \r \n "
.. " Content-Length: " .. # response .. " \r \n "
.. " \r \n "
.. response ;
request.write ( resp ) ;
end
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 request.parser then
local function success_cb ( r )
for k , v in pairs ( r ) do request [ k ] = v ; end
request.url = url_parse ( request.path ) ;
request.body = { request.body } ;
call_callback ( request ) ;
end
local function error_cb ( r )
call_callback ( request , r or " connection-closed " ) ;
destroy_request ( request ) ;
end
request.parser = httpstream_new ( success_cb , error_cb ) ;
end
request.parser : feed ( data ) ;
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 ,
write = function ( ... ) return handler : write ( ... ) ; end , 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.ondisconnect ( 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 " ;
ssl.options = " no_sslv2 " ;
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 ;