From 00807235c4b42f760b4d26f9720396638c8bb60b Mon Sep 17 00:00:00 2001 From: Alexey Shchepin Date: Thu, 25 Jan 2007 05:53:58 +0000 Subject: [PATCH] * src/web/*: Plugin architecture for HTTP modules (thanks to Massimiliano Mirra) SVN Revision: 713 --- ChangeLog | 5 ++ src/web/ejabberd_http.erl | 85 +++++++++++++++++++------- src/web/ejabberd_http_poll.erl | 7 +-- src/web/ejabberd_web.erl | 106 ++++----------------------------- src/web/ejabberd_web_admin.erl | 73 ++++++++++++++++++++++- 5 files changed, 155 insertions(+), 121 deletions(-) diff --git a/ChangeLog b/ChangeLog index 299093426..19ae31380 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,8 @@ +2007-01-25 Alexey Shchepin + + * src/web/*: Plugin architecture for HTTP modules (thanks to + Massimiliano Mirra) + 2007-01-24 Mickael Remond * doc/guide.tex: Documentation for the diff --git a/src/web/ejabberd_http.erl b/src/web/ejabberd_http.erl index 3220925ec..24913de58 100644 --- a/src/web/ejabberd_http.erl +++ b/src/web/ejabberd_http.erl @@ -31,8 +31,15 @@ request_keepalive, request_content_length, request_lang = "en", - use_http_poll = false, - use_web_admin = false, + %% XXX bard: request handlers are configured in + %% ejabberd.cfg under the HTTP service. For example, + %% to have the module test_web handle requests with + %% paths starting with "/test/module": + %% + %% {5280, ejabberd_http, [http_poll, web_admin, + %% {request_handlers, [{["test", "module"], mod_test_web}]}]} + %% + request_handlers = [], end_of_request = false, trail = "" }). @@ -71,16 +78,32 @@ start_link({SockMod, Socket}, Opts) -> _ -> ok end, - UseHTTPPoll = lists:member(http_poll, Opts), - UseWebAdmin = lists:member(web_admin, Opts), - ?DEBUG("S: ~p~n", [{UseHTTPPoll, UseWebAdmin}]), + + %% XXX bard: for backward compatibility: expand web_admin and + %% http_poll in Opts respectively to {["admin"], + %% ejabberd_web_admin} and {["http-poll"], ejabberd_http_poll} + + RequestHandlers = + case lists:keysearch(request_handlers, 1, Opts) of + {value, {request_handlers, H}} -> H; + false -> [] + end ++ + case lists:member(web_admin, Opts) of + true -> [{["admin"], ejabberd_web_admin}]; + false -> [] + end ++ + case lists:member(http_poll, Opts) of + true -> [{["http-poll"], ejabberd_http_poll}]; + false -> [] + end, + ?DEBUG("S: ~p~n", [RequestHandlers]), + ?INFO_MSG("started: ~p", [{SockMod1, Socket1}]), {ok, proc_lib:spawn_link(ejabberd_http, receive_headers, [#state{sockmod = SockMod1, socket = Socket1, - use_http_poll = UseHTTPPoll, - use_web_admin = UseWebAdmin}])}. + request_handlers = RequestHandlers}])}. become_controller(_Pid) -> @@ -192,23 +215,44 @@ process_header(State, Data) -> end, #state{sockmod = SockMod, socket = Socket, - use_http_poll = State#state.use_http_poll, - use_web_admin = State#state.use_web_admin}; + request_handlers = State#state.request_handlers}; _ -> - #state{end_of_request = true} + #state{end_of_request = true, + request_handlers = State#state.request_handlers} end; {error, _Reason} -> - #state{end_of_request = true}; + #state{end_of_request = true, + request_handlers = State#state.request_handlers}; _ -> - #state{end_of_request = true} + #state{end_of_request = true, + request_handlers = State#state.request_handlers} + end. + +%% XXX bard: search through request handlers looking for one that +%% matches the requested URL path, and pass control to it. If none is +%% found, answer with HTTP 404. +process([], _) -> + ejabberd_web:error(not_found); +process(Handlers, Request) -> + [{PathPattern, HandlerModule} | HandlersLeft] = Handlers, + + case lists:prefix(PathPattern, Request#request.path) of + true -> + %% LocalPath is the path "local to the handler", i.e. if + %% the handler was registered to handle "/test/" and the + %% requested path is "/test/foo/bar", the local path is + %% ["foo", "bar"] + LocalPath = lists:nthtail(length(PathPattern), Request#request.path), + HandlerModule:process(LocalPath, Request); + false -> + process(HandlersLeft, Request) end. process_request(#state{request_method = 'GET', request_path = {abs_path, Path}, request_auth = Auth, request_lang = Lang, - use_http_poll = UseHTTPPoll, - use_web_admin = UseWebAdmin} = State) -> + request_handlers = RequestHandlers} = State) -> case (catch url_decode_q_split(Path)) of {'EXIT', _} -> process_request(false); @@ -225,8 +269,11 @@ process_request(#state{request_method = 'GET', q = LQuery, auth = Auth, lang = Lang}, - case ejabberd_web:process_get({UseHTTPPoll, UseWebAdmin}, - Request) of + %% XXX bard: This previously passed control to + %% ejabberd_web:process_get, now passes it to a local + %% procedure (process) that handles dispatching based on + %% URL path prefix. + case process(RequestHandlers, Request) of El when element(1, El) == xmlelement -> make_xhtml_output(State, 200, [], El); {Status, Headers, El} when @@ -247,8 +294,7 @@ process_request(#state{request_method = 'POST', request_lang = Lang, sockmod = SockMod, socket = Socket, - use_http_poll = UseHTTPPoll, - use_web_admin = UseWebAdmin} = State) + request_handlers = RequestHandlers} = State) when is_integer(Len) -> case SockMod of gen_tcp -> @@ -275,8 +321,7 @@ process_request(#state{request_method = 'POST', auth = Auth, data = Data, lang = Lang}, - case ejabberd_web:process_get({UseHTTPPoll, UseWebAdmin}, - Request) of + case process(RequestHandlers, Request) of El when element(1, El) == xmlelement -> make_xhtml_output(State, 200, [], El); {Status, Headers, El} when diff --git a/src/web/ejabberd_http_poll.erl b/src/web/ejabberd_http_poll.erl index d12277449..01268b817 100644 --- a/src/web/ejabberd_http_poll.erl +++ b/src/web/ejabberd_http_poll.erl @@ -24,7 +24,7 @@ setopts/2, controlling_process/2, close/1, - process_request/1]). + process/2]). -include("ejabberd.hrl"). -include("jlib.hrl"). @@ -83,8 +83,7 @@ close({http_poll, FsmRef}) -> catch gen_fsm:sync_send_all_state_event(FsmRef, close). -process_request(#request{path = [], - data = Data} = Request) -> +process([], #request{data = Data} = Request) -> case catch parse_request(Data) of {ok, ID1, Key, NewKey, Packet} -> ID = if @@ -130,7 +129,7 @@ process_request(#request{path = [], _ -> {200, [?CT, {"Set-Cookie", "ID=-2:0; expires=-1"}], ""} end; -process_request(_Request) -> +process(_, _Request) -> {400, [], {xmlelement, "h1", [], [{xmlcdata, "400 Bad Request"}]}}. diff --git a/src/web/ejabberd_web.erl b/src/web/ejabberd_web.erl index 143fbf407..a1d6b6592 100644 --- a/src/web/ejabberd_web.erl +++ b/src/web/ejabberd_web.erl @@ -12,13 +12,18 @@ %% External exports -export([make_xhtml/1, - process_get/2]). + error/1]). -include("ejabberd.hrl"). -include("jlib.hrl"). -include("ejabberd_http.hrl"). +%% XXX bard: there are variants of make_xhtml in ejabberd_http and +%% ejabberd_web_admin. It might be a good idea to centralize it here +%% and also create an ejabberd_web.hrl file holding the macros, so +%% that third parties can use ejabberd_web as an "utility" library. + make_xhtml(Els) -> {xmlelement, "html", [{"xmlns", "http://www.w3.org/1999/xhtml"}, {"xml:lang", "en"}, @@ -48,98 +53,7 @@ make_xhtml(Els) -> {"name", Name}, {"value", Value}])). - -process_get({_, true}, - #request{auth = Auth, - path = ["admin", "server", SHost | RPath], - q = Query, - lang = Lang} = Request) -> - Host = jlib:nameprep(SHost), - case lists:member(Host, ?MYHOSTS) of - true -> - US = case Auth of - {SJID, P} -> - case jlib:string_to_jid(SJID) of - error -> - unauthorized; - #jid{user = U, server = S} -> - case ejabberd_auth:check_password(U, S, P) of - true -> - {U, S}; - false -> - unauthorized - end - end; - _ -> - unauthorized - end, - case US of - {User, Server} -> - case acl:match_rule( - Host, configure, jlib:make_jid(User, Server, "")) of - deny -> - {401, [], make_xhtml([?XC("h1", "Not Allowed")])}; - allow -> - ejabberd_web_admin:process_admin( - Host, Request#request{path = RPath, - us = US}) - end; - unauthorized -> - {401, - [{"WWW-Authenticate", "basic realm=\"ejabberd\""}], - ejabberd_web:make_xhtml([{xmlelement, "h1", [], - [{xmlcdata, "401 Unauthorized"}]}])} - end; - false -> - {404, [], make_xhtml([?XC("h1", "Not found")])} - end; - -process_get({_, true}, - #request{auth = Auth, - path = ["admin" | RPath], - q = Query, - lang = Lang} = Request) -> - US = case Auth of - {SJID, P} -> - case jlib:string_to_jid(SJID) of - error -> - unauthorized; - #jid{user = U, server = S} -> - case ejabberd_auth:check_password(U, S, P) of - true -> - {U, S}; - false -> - unauthorized - end - end; - _ -> - unauthorized - end, - case US of - {User, Server} -> - case acl:match_rule( - global, configure, jlib:make_jid(User, Server, "")) of - deny -> - {401, [], make_xhtml([?XC("h1", "Not Allowed")])}; - allow -> - ejabberd_web_admin:process_admin( - global, Request#request{path = RPath, - us = US}) - end; - unauthorized -> - {401, - [{"WWW-Authenticate", "basic realm=\"ejabberd\""}], - ejabberd_web:make_xhtml([{xmlelement, "h1", [], - [{xmlcdata, "401 Unauthorized"}]}])} - end; - -process_get({true, _}, - #request{path = ["http-poll" | RPath], - q = _Query, - lang = _Lang} = Request) -> - ejabberd_http_poll:process_request(Request#request{path = RPath}); - -process_get(_, _Request) -> - {404, [], make_xhtml([?XC("h1", "Not found")])}. - - +error(not_found) -> + {404, [], make_xhtml([?XC("h1", "404 Not Found")])}; +error(not_allowed) -> + {401, [], make_xhtml([?XC("h1", "401 Unauthorized")])}. diff --git a/src/web/ejabberd_web_admin.erl b/src/web/ejabberd_web_admin.erl index 1c03e50e1..61183f736 100644 --- a/src/web/ejabberd_web_admin.erl +++ b/src/web/ejabberd_web_admin.erl @@ -14,7 +14,9 @@ -vsn('$Revision$ '). %% External exports --export([process_admin/2, +-export([process/2, + %% XXX bard: unexported, since it is only called from process/2 now + %% process_admin/2, list_users/4, list_users_in_diapason/4]). @@ -54,6 +56,75 @@ {"size", Size}])). -define(INPUTST(Type, Name, Value, Size), ?INPUT(Type, Name, ?T(Value), Size)). + +process(["server", SHost | RPath], #request{auth = Auth, + q = Query, + lang = Lang} = Request) -> + Host = jlib:nameprep(SHost), + case lists:member(Host, ?MYHOSTS) of + true -> + case get_auth(Auth) of + {User, Server} -> + case acl:match_rule( + Host, configure, jlib:make_jid(User, Server, "")) of + deny -> + ejabberd_web:error(not_allowed); + allow -> + process_admin( + Host, Request#request{path = RPath, + us = {User, Server}}) + end; + unauthorized -> + {401, + [{"WWW-Authenticate", "basic realm=\"ejabberd\""}], + ejabberd_web:make_xhtml([{xmlelement, "h1", [], + [{xmlcdata, "401 Unauthorized"}]}])} + end; + false -> + ejabberd_web:error(not_found) + end; + +process(RPath, #request{auth = Auth, + q = Query, + lang = Lang} = Request) -> + case get_auth(Auth) of + {User, Server} -> + case acl:match_rule( + global, configure, jlib:make_jid(User, Server, "")) of + deny -> + ejabberd_web:error(not_allowed); + allow -> + process_admin( + global, Request#request{path = RPath, + us = {User, Server}}) + end; + unauthorized -> + %% XXX bard: any reason to send this data now and not + %% always in case of an 401? ought to check http specs... + {401, + [{"WWW-Authenticate", "basic realm=\"ejabberd\""}], + ejabberd_web:make_xhtml([{xmlelement, "h1", [], + [{xmlcdata, "401 Unauthorized"}]}])} + end. + +get_auth(Auth) -> + case Auth of + {SJID, P} -> + case jlib:string_to_jid(SJID) of + error -> + unauthorized; + #jid{user = U, server = S} -> + case ejabberd_auth:check_password(U, S, P) of + true -> + {U, S}; + false -> + unauthorized + end + end; + _ -> + unauthorized + end. + make_xhtml(Els, global, Lang) -> {200, [html], {xmlelement, "html", [{"xmlns", "http://www.w3.org/1999/xhtml"},