且听疯吟 在此记录扯淡的青春
ejabberd: Register

大致流程如图:
ejabberd register

ejabberd 的用户注册主要由 ejabberd_c2s 模块处理。该模块实现了一个有限状态机,即 gen_fsm.
有限状态机可以用如下的公式表示:
State(S) x Event(E) -> Actions(A), State(S')
即在 S 状态时如果有事件 E 发生,那么执行动作 A 后把状态调整到 S'.

以 Register Module 为例来跟踪 ejabberd 中对消息的处理:

启动

ejabberd 启动时初始状态为 wait_for_stream,即等待来自 Client 的消息。此时收到消息由 wait_for_stream 函数处理。

wait_for_stream

由于是注册模块,因此只追踪未鉴权的消息。
此时应向客户端发送 feature 消息,展示当前支持的 sasl 机制,并将当前状态设置为 wait_for_feature_request

case StateData#state.authenticated of
    false ->
        %% ……
        send_element(StateData,  
                         {xmlelement, "stream:features", [],  
                          TLSFeature ++ CompressFeature ++  
                          [{xmlelement, "mechanisms",  
                            [{"xmlns", ?NS_SASL}],  
                            Mechs}] ++  
                          ejabberd_hooks:run_fold(  
                            c2s_stream_features,  
                            Server,  
                            [], [Server])}),
        fsm_next_state(wait_for_feature_request,
                           StateData#state{
                         server = Server,
                         sasl_state = SASLState,
                         lang = Lang});
 ```

### wait_for_feature_request
收到服务端提供的 sasl mechanisms 消息后,由于是注册流程,客户端并不会选择任何一种 mechanism 开始 sasl 认证,而是会向服务端发送如下形式的 iq 消息请求注册参数:

```XML
<iq id="xxxx" to="xxx" type="get"><query xmlns="jabber:iq:register"></query></iq>

wait_for_feature_request 函数获取 xmlns 属性,并一直匹配到 process_unauthenticated_stanza:

wait_for_feature_request({xmlstreamelement, El},
         StateData) ->
#xmlel{name = Name, attrs = Attrs, children = Els} = El,
Zlib = StateData#state.zlib,
TLS = StateData#state.tls,
TLSEnabled = StateData#state.tls_enabled,
TLSRequired = StateData#state.tls_required,
SockMod =
(StateData#state.sockmod):get_sockmod(StateData#state.socket),
case {xml:get_attr_s(<<"xmlns">>, Attrs), Name} of
    %%% ......
  _ ->
  if (SockMod == gen_tcp) and TLSRequired ->
     Lang = StateData#state.lang,
     send_element(StateData,
              ?POLICY_VIOLATION_ERR(Lang,
                        <<"Use of STARTTLS required">>)),
     send_trailer(StateData),
     {stop, normal, StateData};
     true ->
         process_unauthenticated_stanza(StateData, El),
     fsm_next_state(wait_for_feature_request, StateData)
  end
end;

process_unauthenticated_stanza

process_unauthenticated_stanza 函数中,匹配到 iq 消息调用 c2s_unauthenticated_iq (该函数位于 mod_register 模块中)处理。

case jlib:iq_query_info(NewEl) of  
#iq{} = IQ ->  
    Res = ejabberd_hooks:run_fold(c2s_unauthenticated_iq,  
                  StateData#state.server,  
                  empty,  
                  [StateData#state.server, IQ,  
                   StateData#state.ip])

c2s_unauthenticated_iq

该函数实际上通过 hook 的方式调用了 mod_register 模块中的 unauthenticated_iq_register 函数。

unauthenticated_iq_register(_Acc, Server,
            #iq{xmlns = ?NS_REGISTER} = IQ, IP) ->
Address = case IP of
    {A, _Port} -> A;
    _ -> undefined
      end,
ResIQ = process_iq(jlib:make_jid(<<"">>, <<"">>,
                 <<"">>),
           jlib:make_jid(<<"">>, Server, <<"">>), IQ, Address),
Res1 = jlib:replace_from_to(jlib:make_jid(<<"">>,
                      Server, <<"">>),
            jlib:make_jid(<<"">>, <<"">>, <<"">>),
            jlib:iq_to_xml(ResIQ)),
jlib:remove_attr(<<"to">>, Res1);

其中 process_iq 即为处理该 iq 消息的函数,它将消息分为 getset 两种类型:

process_iq(From, To,
   #iq{type = Type, lang = Lang, sub_el = SubEl, id = ID} =
       IQ,
   Source) ->
   %%% ......
case Type of
  set ->
    %%% ......         
  get ->
    %%% ......

客户端首先发送 get 类型的消息,服务端收到后会发送包含注册所需参数的消息给客户端(此时状态会重置为 wait_for_feature_request).
客户端按照参数发送注册 iq 消息给服务端,此时的 type 将设置为 set.经过同样的处理路由,进入注册流程。

IsCaptchaEnabled ->
     case ejabberd_captcha:process_reply(SubEl) of
       ok ->
           case process_xdata_submit(SubEl) of
         {ok, User, Password} ->
             try_register_or_set_password(User, Server,
                          Password, From, IQ,
                          SubEl, Source, Lang,
                          true);
         _ ->
             IQ#iq{type = error,
               sub_el = [SubEl, ?ERR_BAD_REQUEST]}
           end;
       {error, malformed} ->
           IQ#iq{type = error, sub_el = [SubEl, ?ERR_BAD_REQUEST]};
       _ ->
           ErrText = <<"The CAPTCHA verification has failed">>,
           IQ#iq{type = error,
             sub_el = [SubEl, ?ERRT_NOT_ALLOWED(Lang, ErrText)]}
     end;

try_register_or_set_password

由于是注册流程,会进入 try_register 函数中,并调用 ejabberd_auth 模块中的 try_register 函数:

try_register(_User, _Server, <<"">>) ->
{error, not_allowed};
try_register(User, Server, Password) ->
    case is_user_exists(User, Server) of
      true -> {atomic, exists};
      false ->
      case lists:member(jlib:nameprep(Server), ?MYHOSTS) of
        true ->
        Res = lists:foldl(fun (_M, {atomic, ok} = Res) -> Res;
                      (M, _) ->
                      M:try_register(User, Server, Password)
                  end,
                  {error, not_allowed}, auth_modules(Server)),
        case Res of
          {atomic, ok} ->
              ejabberd_hooks:run(register_user, Server,
                     [User, Server]),
              {atomic, ok};
          _ -> Res
        end;
        false -> {error, not_allowed}
      end
    end.

该函数会根据选择的配置来调用不同的模块注册(这些模块都以 ejabberd_auth_ 开头),比如 ejabberd_auth_internal 中的 try_register/3,且在调用注册方法之前会先调用各模块中的 is_user_exists 来判断给出的用户信息是否允许注册:

try_register(User, Server, PasswordList) ->
    LUser = jlib:nodeprep(User),
    LServer = jlib:nameprep(Server),
    Password = iolist_to_binary(PasswordList),
    US = {LUser, LServer},
    if (LUser == error) or (LServer == error) ->
       {error, invalid_jid};
       true ->
       F = fun () ->
               case mnesia:read({passwd, US}) of
             [] ->
                 Password2 = case is_scrammed() and
                        is_binary(Password)
                         of
                       true -> password_to_scram(Password);
                       false -> Password
                     end,
                 mnesia:write(#passwd{us = US,
                          password = Password2}),
                 mnesia:dirty_update_counter(reg_users_counter,
                             LServer, 1),
                 ok;
             [_E] -> exists
               end
           end,
       mnesia:transaction(F)
    end.

至此完成了整个用户的注册流程,状态依然重置为 wait_for_feature_request.


Update: 详细的注册流程