diff --git a/doc/guide.tex b/doc/guide.tex index 6bc11a77c..bb72d30c8 100644 --- a/doc/guide.tex +++ b/doc/guide.tex @@ -1031,7 +1031,7 @@ However, the c2s and s2s connections to the domain \term{example.com} use the fi \item Port 5269 listens for s2s connections with STARTTLS. The socket is set for IPv6 instead of IPv4. \item Port 3478 listens for STUN requests over UDP. \item Port 5280 listens for HTTP requests, and serves the HTTP Poll service. -\item Port 5281 listens for HTTP requests, and serves the Web Admin using HTTPS as explained in +\item Port 5281 listens for HTTP requests, using HTTPS to serve HTTP-Bind (BOSH) and the Web Admin as explained in section~\ref{webadmin}. The socket only listens connections to the IP address 127.0.0.1. \end{itemize} \begin{verbatim} @@ -1060,6 +1060,7 @@ However, the c2s and s2s connections to the domain \term{example.com} use the fi ]}, {{5281, "127.0.0.1"}, ejabberd_http, [ web_admin, + http_bind, tls, {certfile, "/etc/ejabberd/server.pem"}, ]} ] @@ -1637,13 +1638,13 @@ The configurable options are: Full path to a script that generates the image. The default value is an empty string: \term{""} \titem{\{captcha\_host, ProtocolHostPort\}} - Host part of the URL sent to the user, - and the port number where ejabberd listens for CAPTCHA requests. - The URL sent to the user is formed by: \term{http://Host:Port/captcha/} - The default value is: the first hostname configured, and port 5280. - If the port number you specify does not match exactly an ejabberd listener + ProtocolHostPort is a string with the host, and optionally the Protocol and Port number. + It must identify where ejabberd listens for CAPTCHA requests. + The URL sent to the user is formed by: \term{Protocol://Host:Port/captcha/} + The default value is: protocol \term{http}, the first hostname configured, and port \term{80}. + If you specify a port number that does not match exactly an ejabberd listener (because you are using a reverse proxy or other port-forwarding tool), - then specify also the transfer protocol, as seen in the example below. + then you must specify the transfer protocol, as seen in the example below. \end{description} Additionally, an \term{ejabberd\_http} listener must be enabled with the \term{captcha} option. @@ -1656,6 +1657,7 @@ Example configuration: {captcha_cmd, "/lib/ejabberd/priv/bin/captcha.sh"}. {captcha_host, "example.org:5280"}. %% {captcha_host, "https://example.org:443"}. +%% {captcha_host, "http://example.com"}. {listen, [ @@ -3828,10 +3830,11 @@ enables end users to use a \XMPP{} client to: Options: \begin{description} -\titem{\{access, AccessName\}} \ind{options!access}This option can be configured to specify - rules to restrict registration. If a rule returns `deny' on the requested - user name, registration for that user name is denied. (there are no - restrictions by default). +\titem{\{access, AccessName\}} \ind{options!access} + Specify rules to restrict what usernames can be registered and unregistered. + If a rule returns `deny' on the requested username, + registration and unregistration of that user name is denied. + There are no restrictions by default. \titem{\{access\_from, AccessName\}} \ind{options!access\_from}By default, \ejabberd{} doesn't allow to register new accounts from s2s or existing c2s sessions. You can change it by defining access rule in this option. Use with care: allowing registration diff --git a/src/ejabberd.cfg.example b/src/ejabberd.cfg.example index 0912bf559..c776a8cb9 100644 --- a/src/ejabberd.cfg.example +++ b/src/ejabberd.cfg.example @@ -479,6 +479,10 @@ %% %%{captcha_host, "example.org:5280"}. +%% +%% Limit CAPTCHA calls per minute for JID/IP to avoid DoS. +%% +%%{captcha_limit, 5}. %%%. ======= %%%' MODULES diff --git a/src/ejabberd_captcha.erl b/src/ejabberd_captcha.erl index 7c5ea7035..f7b3f4778 100644 --- a/src/ejabberd_captcha.erl +++ b/src/ejabberd_captcha.erl @@ -35,9 +35,9 @@ -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --export([create_captcha/5, build_captcha_html/2, check_captcha/2, +-export([create_captcha/6, build_captcha_html/2, check_captcha/2, process_reply/1, process/2, is_feature_available/0, - create_captcha_x/4, create_captcha_x/5]). + create_captcha_x/5, create_captcha_x/6]). -include("jlib.hrl"). -include("ejabberd.hrl"). @@ -50,8 +50,9 @@ -define(CAPTCHA_TEXT(Lang), translate:translate(Lang, "Enter the text you see")). -define(CAPTCHA_LIFETIME, 120000). % two minutes -define(RPC_TIMEOUT, 5000). +-define(LIMIT_PERIOD, 60*1000*1000). % one minute --record(state, {}). +-record(state, {limits = treap:empty()}). -record(captcha, {id, pid, key, tref, args}). %%==================================================================== @@ -64,10 +65,10 @@ start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). -create_captcha(SID, From, To, Lang, Args) +create_captcha(SID, From, To, Lang, Limiter, Args) when is_list(Lang), is_list(SID), is_record(From, jid), is_record(To, jid) -> - case create_image() of + case create_image(Limiter) of {ok, Type, Key, Image} -> Id = randoms:get_string() ++ "-" ++ ejabberd_cluster:node_id(), B64Image = jlib:encode_base64(binary_to_list(Image)), @@ -96,18 +97,18 @@ create_captcha(SID, From, To, Lang, Args) OOB = {xmlelement, "x", [{"xmlns", ?NS_OOB}], [{xmlelement, "url", [], [{xmlcdata, get_url(Id)}]}]}, Tref = erlang:send_after(?CAPTCHA_LIFETIME, ?MODULE, {remove_id, Id}), - ets:insert(captcha, #captcha{id=Id, pid=self(), key=Key, + ets:insert(captcha, #captcha{id=Id, pid=self(), key=Key, tref=Tref, args=Args}), {ok, Id, [Body, OOB, Captcha, Data]}; - _Err -> - error + Err -> + Err end. -create_captcha_x(SID, To, Lang, HeadEls) -> - create_captcha_x(SID, To, Lang, HeadEls, []). +create_captcha_x(SID, To, Lang, Limiter, HeadEls) -> + create_captcha_x(SID, To, Lang, Limiter, HeadEls, []). -create_captcha_x(SID, To, Lang, HeadEls, TailEls) -> - case create_image() of +create_captcha_x(SID, To, Lang, Limiter, HeadEls, TailEls) -> + case create_image(Limiter) of {ok, Type, Key, Image} -> Id = randoms:get_string() ++ "-" ++ ejabberd_cluster:node_id(), B64Image = jlib:encode_base64(binary_to_list(Image)), @@ -144,8 +145,8 @@ create_captcha_x(SID, To, Lang, HeadEls, TailEls) -> Tref = erlang:send_after(?CAPTCHA_LIFETIME, ?MODULE, {remove_id, Id}), ets:insert(captcha, #captcha{id=Id, key=Key, tref=Tref}), {ok, [Captcha, Data]}; - _ -> - error + Err -> + Err end. %% @spec (Id::string(), Lang::string()) -> {FormEl, {ImgEl, TextEl, IdEl, KeyEl}} | captcha_not_found @@ -242,16 +243,19 @@ process(_Handlers, #request{method='GET', lang=Lang, path=[_, Id]}) -> ejabberd_web:error(not_found) end; -process(_Handlers, #request{method='GET', path=[_, Id, "image"]}) -> +process(_Handlers, #request{method='GET', path=[_, Id, "image"], ip = IP}) -> + {Addr, _Port} = IP, case lookup_captcha(Id) of {ok, #captcha{key=Key}} -> - case create_image(Key) of + case create_image(Addr, Key) of {ok, Type, _, Img} -> {200, [{"Content-Type", Type}, {"Cache-Control", "no-cache"}, {"Last-Modified", httpd_util:rfc1123_date()}], Img}; + {error, limit} -> + ejabberd_web:error(not_allowed); _ -> ejabberd_web:error(not_found) end; @@ -288,6 +292,20 @@ init([]) -> check_captcha_setup(), {ok, #state{}}. +handle_call({is_limited, Limiter, RateLimit}, _From, State) -> + NowPriority = now_priority(), + CleanPriority = NowPriority + ?LIMIT_PERIOD, + Limits = clean_treap(State#state.limits, CleanPriority), + case treap:lookup(Limiter, Limits) of + {ok, _, Rate} when Rate >= RateLimit -> + {reply, true, State#state{limits = Limits}}; + {ok, Priority, Rate} -> + NewLimits = treap:insert(Limiter, Priority, Rate+1, Limits), + {reply, false, State#state{limits = NewLimits}}; + _ -> + NewLimits = treap:insert(Limiter, NowPriority, 1, Limits), + {reply, false, State#state{limits = NewLimits}} + end; handle_call(_Request, _From, State) -> {reply, bad_request, State}. @@ -329,11 +347,22 @@ code_change(_OldVsn, State, _Extra) -> %% Reason = atom() %%-------------------------------------------------------------------- create_image() -> + create_image(undefined). + +create_image(Limiter) -> %% Six numbers from 1 to 9. Key = string:substr(randoms:get_string(), 1, 6), - create_image(Key). + create_image(Limiter, Key). -create_image(Key) -> +create_image(Limiter, Key) -> + case is_limited(Limiter) of + true -> + {error, limit}; + false -> + do_create_image(Key) + end. + +do_create_image(Key) -> FileName = get_prog_name(), Cmd = lists:flatten(io_lib:format("~s ~s", [FileName, Key])), case cmd(Cmd) of @@ -363,22 +392,26 @@ get_prog_name() -> case ejabberd_config:get_local_option(captcha_cmd) of FileName when is_list(FileName) -> FileName; - _ -> + Value when (Value == undefined) or (Value == "") -> ?DEBUG("The option captcha_cmd is not configured, but some " "module wants to use the CAPTCHA feature.", []), - throw({error, option_not_configured_captcha_cmd}) + false end. get_url(Str) -> CaptchaHost = ejabberd_config:get_local_option(captcha_host), case string:tokens(CaptchaHost, ":") of - [TransferProt, Host, PortString] -> - TransferProt ++ ":" ++ Host ++ ":" ++ PortString ++ "/captcha/" ++ Str; + [Host] -> + "http://" ++ Host ++ "/captcha/" ++ Str; + ["http"++_ = TransferProt, Host] -> + TransferProt ++ ":" ++ Host ++ "/captcha/" ++ Str; [Host, PortString] -> TransferProt = atom_to_list(get_transfer_protocol(PortString)), TransferProt ++ "://" ++ Host ++ ":" ++ PortString ++ "/captcha/" ++ Str; + [TransferProt, Host, PortString] -> + TransferProt ++ ":" ++ Host ++ ":" ++ PortString ++ "/captcha/" ++ Str; _ -> - "http://" ++ ?MYNAME ++ ":5280/captcha/" ++ Str + "http://" ++ ?MYNAME ++ "/captcha/" ++ Str end. get_transfer_protocol(PortString) -> @@ -416,6 +449,25 @@ get_captcha_transfer_protocol([{{_Port, _Ip, tcp}, ejabberd_http, Opts} get_captcha_transfer_protocol([_ | Listeners]) -> get_captcha_transfer_protocol(Listeners). +is_limited(undefined) -> + false; +is_limited(Limiter) -> + case ejabberd_config:get_local_option(captcha_limit) of + Int when is_integer(Int), Int > 0 -> + case catch gen_server:call(?MODULE, {is_limited, Limiter, Int}, + 5000) of + true -> + true; + false -> + false; + Err -> + ?ERROR_MSG("Call failed: ~p", [Err]), + false + end; + _ -> + false + end. + %%-------------------------------------------------------------------- %% Function: cmd(Cmd) -> Data | {error, Reason} %% Cmd = string() @@ -464,28 +516,22 @@ return(Port, TRef, Result) -> catch port_close(Port), Result. -is_feature_enabled() -> - try get_prog_name() of - Prog when is_list(Prog) -> true - catch - _:_ -> false - end. - is_feature_available() -> - case is_feature_enabled() of - false -> false; - true -> - case create_image() of - {ok, _, _, _} -> true; - _Error -> false - end + case get_prog_name() of + Prog when is_list(Prog) -> true; + false -> false end. check_captcha_setup() -> - case is_feature_enabled() andalso not is_feature_available() of + AbleToGenerateCaptcha = case create_image() of + {ok, _, _, _} -> true; + _Error -> false + end, + case is_feature_available() andalso not AbleToGenerateCaptcha of true -> ?CRITICAL_MSG("Captcha is enabled in the option captcha_cmd, " - "but it can't generate images.", []); + "but it can't generate images.", []), + throw({error, captcha_cmd_enabled_but_fails}); false -> ok end. @@ -537,3 +583,21 @@ do_check_captcha(Id, ProvidedKey) -> _ -> captcha_not_found end. + +clean_treap(Treap, CleanPriority) -> + case treap:is_empty(Treap) of + true -> + Treap; + false -> + {_Key, Priority, _Value} = treap:get_root(Treap), + if + Priority > CleanPriority -> + clean_treap(treap:delete_root(Treap), CleanPriority); + true -> + Treap + end + end. + +now_priority() -> + {MSec, Sec, USec} = now(), + -((MSec*1000000 + Sec)*1000000 + USec). diff --git a/src/ejabberd_config.erl b/src/ejabberd_config.erl index 4ec848b23..1609b447d 100644 --- a/src/ejabberd_config.erl +++ b/src/ejabberd_config.erl @@ -431,6 +431,8 @@ process_term(Term, State) -> add_option(captcha_cmd, Cmd, State); {captcha_host, Host} -> add_option(captcha_host, Host, State); + {captcha_limit, Limit} -> + add_option(captcha_limit, Limit, State); {ejabberdctl_access_commands, ACs} -> add_option(ejabberdctl_access_commands, ACs, State); {loglevel, Loglevel} -> diff --git a/src/expat_erl.c b/src/expat_erl.c index 88aaf2b1e..e38420f5f 100644 --- a/src/expat_erl.c +++ b/src/expat_erl.c @@ -35,6 +35,7 @@ #define PARSE_FINAL_COMMAND 1 ei_x_buff event_buf; +ei_x_buff xmlns_buf; typedef struct { ErlDrvPort port; @@ -43,6 +44,32 @@ typedef struct { static XML_Memory_Handling_Suite ms = {driver_alloc, driver_realloc, driver_free}; +void encode_name(const XML_Char *name) +{ + char *name_start; + char *prefix_start; + char *buf; + int name_len, prefix_len, buf_len; + + if ((name_start = strchr(name, '\n'))) { + if ((prefix_start = strchr(name_start+1, '\n'))) { + name_len = prefix_start - name_start; + prefix_len = strlen(prefix_start+1); + buf_len = prefix_len + name_len; + buf = driver_alloc(buf_len); + memcpy(buf, prefix_start+1, prefix_len); + memcpy(buf+prefix_len, name_start, name_len); + buf[prefix_len] = ':'; + ei_x_encode_string_len(&event_buf, buf, buf_len); + driver_free(buf); + } else { + ei_x_encode_string(&event_buf, name_start+1); + }; + } else { + ei_x_encode_string(&event_buf, name); + } +} + void *erlXML_StartElementHandler(expat_data *d, const XML_Char *name, const XML_Char **atts) @@ -53,7 +80,10 @@ void *erlXML_StartElementHandler(expat_data *d, ei_x_encode_tuple_header(&event_buf, 2); ei_x_encode_long(&event_buf, XML_START); ei_x_encode_tuple_header(&event_buf, 2); - ei_x_encode_string(&event_buf, name); + encode_name(name); + ei_x_append(&event_buf, &xmlns_buf); + ei_x_free(&xmlns_buf); + ei_x_new(&xmlns_buf); for (i = 0; atts[i]; i += 2) {} @@ -64,7 +94,7 @@ void *erlXML_StartElementHandler(expat_data *d, for (i = 0; atts[i]; i += 2) { ei_x_encode_tuple_header(&event_buf, 2); - ei_x_encode_string(&event_buf, atts[i]); + encode_name(atts[i]); ei_x_encode_string(&event_buf, atts[i+1]); } } @@ -80,7 +110,7 @@ void *erlXML_EndElementHandler(expat_data *d, ei_x_encode_list_header(&event_buf, 1); ei_x_encode_tuple_header(&event_buf, 2); ei_x_encode_long(&event_buf, XML_END); - ei_x_encode_string(&event_buf, name); + encode_name(name); return NULL; } @@ -95,12 +125,35 @@ void *erlXML_CharacterDataHandler(expat_data *d, return NULL; } +void *erlXML_StartNamespaceDeclHandler(expat_data *d, + const XML_Char *prefix, + const XML_Char *uri) +{ + int prefix_len; + char *buf; + + ei_x_encode_list_header(&xmlns_buf, 1); + ei_x_encode_tuple_header(&xmlns_buf, 2); + if (prefix) { + prefix_len = strlen(prefix); + buf = driver_alloc(7 + prefix_len); + strcpy(buf, "xmlns:"); + strcpy(buf+6, prefix); + ei_x_encode_string(&xmlns_buf, buf); + driver_free(buf); + } else { + ei_x_encode_string(&xmlns_buf, "xmlns"); + }; + ei_x_encode_string(&xmlns_buf, uri); + + return NULL; +} static ErlDrvData expat_erl_start(ErlDrvPort port, char *buff) { expat_data* d = (expat_data*)driver_alloc(sizeof(expat_data)); d->port = port; - d->parser = XML_ParserCreate_MM("UTF-8", &ms, NULL); + d->parser = XML_ParserCreate_MM("UTF-8", &ms, "\n"); XML_SetUserData(d->parser, d); set_port_control_flags(port, PORT_CONTROL_FLAG_BINARY); @@ -112,6 +165,9 @@ static ErlDrvData expat_erl_start(ErlDrvPort port, char *buff) XML_SetCharacterDataHandler( d->parser, (XML_CharacterDataHandler)erlXML_CharacterDataHandler); + XML_SetStartNamespaceDeclHandler( + d->parser, (XML_StartNamespaceDeclHandler) erlXML_StartNamespaceDeclHandler); + XML_SetReturnNSTriplet(d->parser, 1); return (ErlDrvData)d; } @@ -138,6 +194,7 @@ static int expat_erl_control(ErlDrvData drv_data, case PARSE_COMMAND: case PARSE_FINAL_COMMAND: ei_x_new_with_version(&event_buf); + ei_x_new(&xmlns_buf); #ifdef ENABLE_FLASH_HACK /* Flash hack - Flash clients send a null byte after the stanza. Remove that... */ { @@ -190,6 +247,7 @@ static int expat_erl_control(ErlDrvData drv_data, memcpy(b->orig_bytes, event_buf.buff, size); ei_x_free(&event_buf); + ei_x_free(&xmlns_buf); *rbuf = (char *)b; return size; diff --git a/src/extauth.erl b/src/extauth.erl index 3b2e7db64..f49392983 100644 --- a/src/extauth.erl +++ b/src/extauth.erl @@ -45,11 +45,18 @@ start(Host, ExtPrg) -> lists:foreach( fun(This) -> - spawn(?MODULE, init, [get_process_name(Host, This), ExtPrg]) + start_instance(get_process_name(Host, This), ExtPrg) end, lists:seq(0, get_instances(Host)-1) ). +start_instance(ProcessName, ExtPrg) -> + spawn(?MODULE, init, [ProcessName, ExtPrg]). + +restart_instance(ProcessName, ExtPrg) -> + unregister(ProcessName), + start_instance(ProcessName, ExtPrg). + init(ProcessName, ExtPrg) -> register(ProcessName, self()), process_flag(trap_exit,true), @@ -125,8 +132,7 @@ loop(Port, Timeout, ProcessName, ExtPrg) -> Timeout -> ?ERROR_MSG("extauth call '~p' didn't receive response", [Msg]), Caller ! {eauth, false}, - unregister(ProcessName), - Pid = spawn(?MODULE, init, [ProcessName, ExtPrg]), + Pid = restart_instance(ProcessName, ExtPrg), flush_buffer_and_forward_messages(Pid), exit(port_terminated) end; @@ -137,7 +143,9 @@ loop(Port, Timeout, ProcessName, ExtPrg) -> exit(normal) end; {'EXIT', Port, Reason} -> - ?CRITICAL_MSG("~p ~n", [Reason]), + ?CRITICAL_MSG("extauth script has exitted abruptly with reason '~p'", [Reason]), + Pid = restart_instance(ProcessName, ExtPrg), + flush_buffer_and_forward_messages(Pid), exit(port_terminated) end. diff --git a/src/mod_muc/mod_muc_room.erl b/src/mod_muc/mod_muc_room.erl index 2891cf261..5bf88a675 100644 --- a/src/mod_muc/mod_muc_room.erl +++ b/src/mod_muc/mod_muc_room.erl @@ -1666,15 +1666,25 @@ add_new_user(From, Nick, {xmlelement, _, Attrs, Els} = Packet, StateData) -> SID = xml:get_attr_s("id", Attrs), RoomJID = StateData#state.jid, To = jlib:jid_replace_resource(RoomJID, Nick), + Limiter = {From#jid.luser, From#jid.lserver}, case ejabberd_captcha:create_captcha( - SID, RoomJID, To, Lang, From) of + SID, RoomJID, To, Lang, Limiter, From) of {ok, ID, CaptchaEls} -> MsgPkt = {xmlelement, "message", [{"id", ID}], CaptchaEls}, Robots = ?DICT:store(From, {Nick, Packet}, StateData#state.robots), ejabberd_router:route(RoomJID, From, MsgPkt), StateData#state{robots = Robots}; - error -> + {error, limit} -> + ErrText = "Too many CAPTCHA requests", + Err = jlib:make_error_reply( + Packet, ?ERRT_RESOURCE_CONSTRAINT(Lang, ErrText)), + ejabberd_router:route( % TODO: s/Nick/""/ + jlib:jid_replace_resource( + StateData#state.jid, Nick), + From, Err), + StateData; + _ -> ErrText = "Unable to generate a captcha", Err = jlib:make_error_reply( Packet, ?ERRT_INTERNAL_SERVER_ERROR(Lang, ErrText)), @@ -1726,7 +1736,24 @@ check_captcha(Affiliation, From, StateData) -> {ok, passed} -> true; _ -> - captcha_required + WList = (StateData#state.config)#config.captcha_whitelist, + #jid{luser = U, lserver = S, lresource = R} = From, + case ?SETS:is_element({U, S, R}, WList) of + true -> + true; + false -> + case ?SETS:is_element({U, S, ""}, WList) of + true -> + true; + false -> + case ?SETS:is_element({"", S, ""}, WList) of + true -> + true; + false -> + captcha_required + end + end + end end; _ -> true @@ -2526,6 +2553,11 @@ can_change_ra(_FAffiliation, _FRole, %% A room owner tries to add as persistent owner a %% participant that is already owner because he is MUC admin true; +can_change_ra(_FAffiliation, _FRole, + _TAffiliation, _TRole, + _RoleorAffiliation, _Value, owner) -> + %% Nobody can decrease MUC admin's role/affiliation + false; can_change_ra(_FAffiliation, _FRole, TAffiliation, _TRole, affiliation, Value, _ServiceAf) @@ -2915,6 +2947,13 @@ is_password_settings_correct(XEl, StateData) -> -define(PRIVATEXFIELD(Label, Var, Val), ?XFIELD("text-private", Label, Var, Val)). +-define(JIDMULTIXFIELD(Label, Var, JIDList), + {xmlelement, "field", [{"type", "jid-multi"}, + {"label", translate:translate(Lang, Label)}, + {"var", Var}], + [{xmlelement, "value", [], [{xmlcdata, jlib:jid_to_string(JID)}]} + || JID <- JIDList]}). + get_default_room_maxusers(RoomState) -> DefRoomOpts = gen_mod:get_module_opt(RoomState#state.server_host, mod_muc, default_room_options, []), RoomState2 = set_opts(DefRoomOpts, RoomState), @@ -3035,6 +3074,9 @@ get_config(Lang, StateData, From) -> Config#config.captcha_protected)]; false -> [] end ++ + [?JIDMULTIXFIELD("Exclude Jabber IDs from CAPTCHA challenge", + "muc#roomconfig_captcha_whitelist", + ?SETS:to_list(Config#config.captcha_whitelist))] ++ case mod_muc_log:check_access_log( StateData#state.server_host, From) of allow -> @@ -3104,6 +3146,18 @@ set_config(XEl, StateData) -> -define(SET_STRING_XOPT(Opt, Val), set_xoption(Opts, Config#config{Opt = Val})). +-define(SET_JIDMULTI_XOPT(Opt, Vals), + begin + Set = lists:foldl( + fun({U, S, R}, Set1) -> + ?SETS:add_element({U, S, R}, Set1); + (#jid{luser = U, lserver = S, lresource = R}, Set1) -> + ?SETS:add_element({U, S, R}, Set1); + (_, Set1) -> + Set1 + end, ?SETS:empty(), Vals), + set_xoption(Opts, Config#config{Opt = Set}) + end). set_xoption([], Config) -> Config; @@ -3161,6 +3215,9 @@ set_xoption([{"muc#roomconfig_maxusers", [Val]} | Opts], Config) -> end; set_xoption([{"muc#roomconfig_enablelogging", [Val]} | Opts], Config) -> ?SET_BOOL_XOPT(logging, Val); +set_xoption([{"muc#roomconfig_captcha_whitelist", Vals} | Opts], Config) -> + JIDs = [jlib:string_to_jid(Val) || Val <- Vals], + ?SET_JIDMULTI_XOPT(captcha_whitelist, JIDs); set_xoption([{"FORM_TYPE", _} | Opts], Config) -> %% Ignore our FORM_TYPE set_xoption(Opts, Config); @@ -3230,6 +3287,7 @@ set_opts([{Opt, Val} | Opts], StateData) -> password -> StateData#state{config = (StateData#state.config)#config{password = Val}}; anonymous -> StateData#state{config = (StateData#state.config)#config{anonymous = Val}}; logging -> StateData#state{config = (StateData#state.config)#config{logging = Val}}; + captcha_whitelist -> StateData#state{config = (StateData#state.config)#config{captcha_whitelist = ?SETS:from_list(Val)}}; max_users -> ServiceMaxUsers = get_service_max_users(StateData), MaxUsers = if @@ -3274,6 +3332,8 @@ make_opts(StateData) -> ?MAKE_CONFIG_OPT(anonymous), ?MAKE_CONFIG_OPT(logging), ?MAKE_CONFIG_OPT(max_users), + {captcha_whitelist, + ?SETS:to_list((StateData#state.config)#config.captcha_whitelist)}, {affiliations, ?DICT:to_list(StateData#state.affiliations)}, {subject, StateData#state.subject}, {subject_author, StateData#state.subject_author} diff --git a/src/mod_muc/mod_muc_room.hrl b/src/mod_muc/mod_muc_room.hrl index eb5060e43..192ecf845 100644 --- a/src/mod_muc/mod_muc_room.hrl +++ b/src/mod_muc/mod_muc_room.hrl @@ -45,7 +45,8 @@ password = "", anonymous = true, max_users = ?MAX_USERS_DEFAULT, - logging = false + logging = false, + captcha_whitelist = ?SETS:empty() }). -record(user, {jid, diff --git a/src/mod_privacy_odbc.erl b/src/mod_privacy_odbc.erl index 64543faa8..2df9ee27a 100644 --- a/src/mod_privacy_odbc.erl +++ b/src/mod_privacy_odbc.erl @@ -751,9 +751,9 @@ item_to_raw(#listitem{type = Type, none -> {"n", ""}; jid -> - {"j", jlib:jid_to_string(Value)}; + {"j", ejabberd_odbc:escape(jlib:jid_to_string(Value))}; group -> - {"g", Value}; + {"g", ejabberd_odbc:escape(Value)}; subscription -> case Value of none -> diff --git a/src/mod_register.erl b/src/mod_register.erl index 4b90be8df..048678373 100644 --- a/src/mod_register.erl +++ b/src/mod_register.erl @@ -106,8 +106,10 @@ process_iq(From, To, PTag = xml:get_subtag(SubEl, "password"), RTag = xml:get_subtag(SubEl, "remove"), Server = To#jid.lserver, + Access = gen_mod:get_module_opt(Server, ?MODULE, access, all), + AllowRemove = (allow == acl:match_rule(Server, Access, From)), if - (UTag /= false) and (RTag /= false) -> + (UTag /= false) and (RTag /= false) and AllowRemove -> User = xml:get_tag_cdata(UTag), case From of #jid{user = User, lserver = Server} -> @@ -148,7 +150,7 @@ process_iq(From, To, sub_el = [SubEl, ?ERR_BAD_REQUEST]} end end; - (UTag == false) and (RTag /= false) -> + (UTag == false) and (RTag /= false) and AllowRemove -> case From of #jid{user = User, lserver = Server, @@ -234,13 +236,18 @@ process_iq(From, To, {"var", "password"}], [{xmlelement, "required", [], []}]}, case ejabberd_captcha:create_captcha_x( - ID, To, Lang, [InstrEl, UField, PField]) of + ID, To, Lang, Source, [InstrEl, UField, PField]) of {ok, CaptchaEls} -> IQ#iq{type = result, sub_el = [{xmlelement, "query", [{"xmlns", "jabber:iq:register"}], [TopInstrEl | CaptchaEls]}]}; - error -> + {error, limit} -> + ErrText = "Too many CAPTCHA requests", + IQ#iq{type = error, + sub_el = [SubEl, ?ERRT_RESOURCE_CONSTRAINT( + Lang, ErrText)]}; + _Err -> ErrText = "Unable to generate a CAPTCHA", IQ#iq{type = error, sub_el = [SubEl, ?ERRT_INTERNAL_SERVER_ERROR( diff --git a/src/mod_roster.erl b/src/mod_roster.erl index 6ffbca2fe..63162b4d0 100644 --- a/src/mod_roster.erl +++ b/src/mod_roster.erl @@ -161,7 +161,7 @@ get_versioning_feature(Acc, Host) -> Feature = {xmlelement, "ver", [{"xmlns", ?NS_ROSTER_VER}], - [{xmlelement, "optional", [], []}]}, + []}, [Feature | Acc]; false -> [] end. @@ -411,10 +411,17 @@ push_item(User, Server, From, Item) -> % TODO: don't push to those who didn't load roster push_item(User, Server, Resource, From, Item) -> + push_item(User, Server, Resource, From, Item, not_found). + +push_item(User, Server, Resource, From, Item, RosterVersion) -> + ExtraAttrs = case RosterVersion of + not_found -> []; + _ -> [{"ver", RosterVersion}] + end, ResIQ = #iq{type = set, xmlns = ?NS_ROSTER, id = "push" ++ randoms:get_string(), sub_el = [{xmlelement, "query", - [{"xmlns", ?NS_ROSTER}], + [{"xmlns", ?NS_ROSTER}|ExtraAttrs], [item_to_xml(Item)]}]}, ejabberd_router:route( From, @@ -425,21 +432,9 @@ push_item(User, Server, Resource, From, Item) -> %% TODO: don't push to those who didn't load roster push_item_version(Server, User, From, Item, RosterVersion) -> lists:foreach(fun(Resource) -> - push_item_version(User, Server, Resource, From, Item, RosterVersion) + push_item(User, Server, Resource, From, Item, RosterVersion) end, ejabberd_sm:get_user_resources(User, Server)). -push_item_version(User, Server, Resource, From, Item, RosterVersion) -> - IQPush = #iq{type = 'set', xmlns = ?NS_ROSTER, - id = "push" ++ randoms:get_string(), - sub_el = [{xmlelement, "query", - [{"xmlns", ?NS_ROSTER}, - {"ver", RosterVersion}], - [item_to_xml(Item)]}]}, - ejabberd_router:route( - From, - jlib:make_jid(User, Server, Resource), - jlib:iq_to_xml(IQPush)). - get_subscription_lists(_, User, Server) -> LUser = jlib:nodeprep(User), LServer = jlib:nameprep(Server), diff --git a/src/odbc/mssql2000.sql b/src/odbc/mssql2000.sql index 151d474a5..d4a70989d 100644 --- a/src/odbc/mssql2000.sql +++ b/src/odbc/mssql2000.sql @@ -597,6 +597,40 @@ BEGIN END GO +/******************************************************************/ +/****** Object: StoredProcedure [dbo].[set_roster_version] **/ +/** Update users roster_version **/ +/******************************************************************/ +CREATE PROCEDURE [dbo].[set_roster_version] + @Username varchar(200), + @Version varchar(50) +AS +BEGIN + IF EXISTS (SELECT username FROM roster_version WITH (NOLOCK) WHERE username=@Username) + BEGIN + UPDATE roster_version SET username=@Username, version=@Version WHERE username=@Username; + END + ELSE + BEGIN + INSERT INTO roster_version (username, version) VALUES (@Username, @Version); + END +END +GO + +/******************************************************************/ +/****** Object: StoredProcedure [dbo].[get_roster_version] **/ +/** Retrive the user roster_version **/ +/******************************************************************/ +CREATE PROCEDURE [dbo].[get_roster_version] + @Username varchar(200) +AS +BEGIN + SELECT roster_version.version as version + FROM roster_version WITH (NOLOCK) + WHERE username=@Username; +END +GO + /***************************************************************/ /****** Object: StoredProcedure [dbo].[clean_spool_msg] ******/ /** Delete messages older that 3 days from spool **/ diff --git a/src/odbc/mssql2005.sql b/src/odbc/mssql2005.sql index 8420b7690..911207078 100644 --- a/src/odbc/mssql2005.sql +++ b/src/odbc/mssql2005.sql @@ -918,6 +918,40 @@ BEGIN END GO +/******************************************************************/ +/****** Object: StoredProcedure [dbo].[set_roster_version] **/ +/** Update users roster_version **/ +/******************************************************************/ +CREATE PROCEDURE [dbo].[set_roster_version] + @Username varchar(200), + @Version varchar(8000) +AS +BEGIN + IF EXISTS (SELECT username FROM roster_version WITH (NOLOCK) WHERE username=@Username) + BEGIN + UPDATE roster_version SET username=@Username, version=@Version WHERE username=@Username; + END + ELSE + BEGIN + INSERT INTO roster_version (username, version) VALUES (@Username, @Version); + END +END +GO + +/******************************************************************/ +/****** Object: StoredProcedure [dbo].[get_roster_version] **/ +/** Retrive the user roster_version **/ +/******************************************************************/ +CREATE PROCEDURE [dbo].[get_roster_version] + @Username varchar(200) +AS +BEGIN + SELECT roster_version.version as version + FROM roster_version WITH (NOLOCK) + WHERE username=@Username; +END +GO + /***************************************************************/ /****** Object: StoredProcedure [dbo].[clean_spool_msg] ******/ /** Delete messages older that 3 days from spool **/ diff --git a/src/odbc/odbc_queries.erl b/src/odbc/odbc_queries.erl index 3e3a29422..b614fd790 100644 --- a/src/odbc/odbc_queries.erl +++ b/src/odbc/odbc_queries.erl @@ -90,6 +90,8 @@ -define(generic, true). -endif. +-include("ejabberd.hrl"). + %% Almost a copy of string:join/2. %% We use this version because string:join/2 is relatively %% new function (introduced in R12B-0). @@ -882,8 +884,14 @@ count_records_where(LServer, Table, WhereClause) -> ["select count(*) from ", Table, " with (nolock) ", WhereClause]). get_roster_version(LServer, LUser) -> - ejabberd_odbc:sql_query(LServer, - ["select version from dbo.roster_version with (nolock) where username = '", LUser, "'"]). -set_roster_version(LUser, Version) -> - update_t("dbo.roster_version", ["username", "version"], [LUser, Version], ["username = '", LUser, "'"]). + ejabberd_odbc:sql_query( + LServer, + ["EXECUTE dbo.get_roster_version '", LUser, "'"]). + +set_roster_version(Username, Version) -> + %% This function doesn't know the vhost, so we hope it's the first one defined: + LServer = ?MYNAME, + ejabberd_odbc:sql_query( + LServer, + ["EXECUTE dbo.set_roster_version '", Username, "', '", Version, "'"]). -endif. diff --git a/src/web/ejabberd_http.erl b/src/web/ejabberd_http.erl index a34aea993..fb38b17a4 100644 --- a/src/web/ejabberd_http.erl +++ b/src/web/ejabberd_http.erl @@ -636,7 +636,13 @@ make_xhtml_output(State, Status, Headers, XHTML) -> end, HeadersOut), SL = [Version, integer_to_list(Status), " ", code_to_phrase(Status), "\r\n"], - [SL, H, "\r\n", Data]. + + Data2 = case State#state.request_method of + 'HEAD' -> ""; + _ -> Data + end, + + [SL, H, "\r\n", Data2]. make_text_output(State, Status, Headers, Text) when is_list(Text) -> make_text_output(State, Status, Headers, list_to_binary(Text)); @@ -673,7 +679,13 @@ make_text_output(State, Status, Headers, Data) when is_binary(Data) -> end, HeadersOut), SL = [Version, integer_to_list(Status), " ", code_to_phrase(Status), "\r\n"], - [SL, H, "\r\n", Data]. + + Data2 = case State#state.request_method of + 'HEAD' -> ""; + _ -> Data + end, + + [SL, H, "\r\n", Data2]. parse_lang(Langs) -> diff --git a/src/web/ejabberd_web_admin.erl b/src/web/ejabberd_web_admin.erl index 90e301401..f3809c15e 100644 --- a/src/web/ejabberd_web_admin.erl +++ b/src/web/ejabberd_web_admin.erl @@ -64,11 +64,15 @@ get_acl_rule(["additions.js"],_) -> {"localhost", [all]}; get_acl_rule(["vhosts"],_) -> {"localhost", [all]}; %% The pages of a vhost are only accesible if the user is admin of that vhost: -get_acl_rule(["server", VHost | _RPath], 'GET') -> {VHost, [configure, webadmin_view]}; +get_acl_rule(["server", VHost | _RPath], Method) + when Method=:='GET' orelse Method=:='HEAD' -> + {VHost, [configure, webadmin_view]}; get_acl_rule(["server", VHost | _RPath], 'POST') -> {VHost, [configure]}; %% Default rule: only global admins can access any other random page -get_acl_rule(_RPath, 'GET') -> {global, [configure, webadmin_view]}; +get_acl_rule(_RPath, Method) + when Method=:='GET' orelse Method=:='HEAD' -> + {global, [configure, webadmin_view]}; get_acl_rule(_RPath, 'POST') -> {global, [configure]}. is_acl_match(Host, Rules, Jid) -> diff --git a/src/web/mod_register_web.erl b/src/web/mod_register_web.erl index 2c6fda28f..98ee52fb9 100644 --- a/src/web/mod_register_web.erl +++ b/src/web/mod_register_web.erl @@ -86,8 +86,9 @@ process([], #request{method = 'GET', lang = Lang}) -> process(["register.css"], #request{method = 'GET'}) -> serve_css(); -process(["new"], #request{method = 'GET', lang = Lang, host = Host}) -> - form_new_get(Host, Lang); +process(["new"], #request{method = 'GET', lang = Lang, host = Host, ip = IP}) -> + {Addr, _Port} = IP, + form_new_get(Host, Lang, Addr); process(["delete"], #request{method = 'GET', lang = Lang, host = Host}) -> form_del_get(Host, Lang); @@ -185,8 +186,8 @@ index_page(Lang) -> %%% Formulary new account GET %%%---------------------------------------------------------------------- -form_new_get(Host, Lang) -> - CaptchaEls = build_captcha_li_list(Lang), +form_new_get(Host, Lang, IP) -> + CaptchaEls = build_captcha_li_list(Lang, IP), HeadEls = [ ?XCT("title", "Register a Jabber account"), ?XA("link", @@ -336,27 +337,31 @@ form_new_post(Username, Host, Password, {Id, Key}) -> %%% Formulary Captcha support for new GET/POST %%%---------------------------------------------------------------------- -build_captcha_li_list(Lang) -> +build_captcha_li_list(Lang, IP) -> case ejabberd_captcha:is_feature_available() of - true -> build_captcha_li_list2(Lang); + true -> build_captcha_li_list2(Lang, IP); false -> [] end. -build_captcha_li_list2(Lang) -> - Id = randoms:get_string(), +build_captcha_li_list2(Lang, IP) -> SID = "", From = #jid{user = "", server = "test", resource = ""}, To = #jid{user = "", server = "test", resource = ""}, Args = [], - ejabberd_captcha:create_captcha(Id, SID, From, To, Lang, Args), - {_, {CImg,CText,CId,CKey}} = ejabberd_captcha:build_captcha_html(Id, Lang), - [?XE("li", [CText, - ?C(" "), - CId, - CKey, - ?BR, - CImg] - )]. + case ejabberd_captcha:create_captcha(SID, From, To, Lang, IP, Args) of + {ok, Id, _} -> + {_, {CImg,CText,CId,CKey}} = + ejabberd_captcha:build_captcha_html(Id, Lang), + [?XE("li", [CText, + ?C(" "), + CId, + CKey, + ?BR, + CImg] + )]; + _ -> + [] + end. %%%---------------------------------------------------------------------- %%% Formulary change password GET