XMPP使用SASL进行登录验证。ejabberd中模仿CyrusSASL库自己实现了SASL协议,API用法与CyrusSASL类似。
ejabberd启动时,ejabberd_app:start/0会调用cyrsasl:start/0。它创建了一个ets表sasl_mechanism
,然后调用各个SASL机制模块的start函数。
1 | start() -> |
各个SASL机制模块的start/1函数会调用cyrsasl:register_mechanism/3, 参数分别为SASL机制名称,机制处理模块,机制支持的密码存储类型。register_mechanism会将机制信息添加到sasl_mechanism表中。
1 | start(_Opts) -> |
1 | register_mechanism(Mechanism, Module, PasswordType) -> |
客户端连接到ejabberd后,ejabberd会创建两个进程。一个ejabberd_c2s进程来处理连接状态并向客户端发送数据,另一个ejabberd_receiver进程接收客户端数据。ejabberd_c2s进程实现了gen_fsm行为,有限状态机的初始状态为wait_for_stream。ejabberd_c2s:init/1的简化代码逻辑为:
1 | init([{SockMod, Socket}, Opts]) -> |
ejabberd_receiver进程解析到XML流头之后会调用gen_fsm:send_event向ejabberd_c2s进程发送消息。
1 | process_data([Element|Els], #state{c2s_pid = C2SPid} = State) |
ejabberd_c2s进程接收到{xmlstreamstart, _Name, Attrs}
消息后,调用状态函数wait_for_stream/2来处理。wait_for_stream在一系列正确性校验通过之后,回应给客户端XML流头。如果这个XML流之前还没有通过登录验证,则进行登录验证过程。简化的代码逻辑如下:
1 | case StateData#state.authenticated of |
首先调用cyrsasl:server_new/7创建一个SASL验证状态, 其中存储了3个用于密码校验的回调函数。
1 | server_new(Service, ServerFQDN, UserRealm, _SecFlags, |
wait_for_stream函数接着调用cyrsasl:listmech/1获取当前域名所支持的SASL验证机制。
1 | listmech(Host) -> |
listmech函数调用ejabberd_auth:store_type/1从ejabberd.cfg文件获取密码存储格式(auth_password_format)的配置。从sasl_mechanism表中查询出支持该密码存储格式的SASL机制。wait_for_stream函数将这些机制组织成XMPP协议格式发送回客户端,将当前进程状态改为wait_for_feature_request。比如,发送的机制列表为:
1 | <stream:features> |
客户端从其中选择一个机制并发送给ejabberd服务器。如:
1 | <auth xmlns="urn:ietf:params:xml:ns:xmpp-sasl" mechanism="PLAIN">AGFhYQAxMjM=</auth> |
ejabberd_receiver进程解析完这个XML元素后,发送消息{xmlstreamelement, El}
给ejabberd_c2s进程。ejabberd_c2s进程调用wait_for_feature_request函数进行处理。
1 | Mech = xml:get_attr_s("mechanism", Attrs), |
ejabberd_c2s进程判断收到的XML元素是auth请求后,从请求中获取客户端选择的机制mechanism,读取客户端发送的信息,然后调用cyrsasl:server_start/3。
1 | server_start(State, Mech, ClientIn) -> |
server_start从sasl_mechanism表中查询出机制模块,并调用机制模块的mech_new/4。这个函数会创建一个机制本身的状态结构。如,PLAIN机制模块的mech_new/4:
1 | mech_new(_Host, _GetPassword, CheckPassword, _CheckPasswordDigest) -> |
server_start函数将这个机制状态保存到SASL状态结构里的mech_state字段,调用server_step。而server_step则调用机制模块的mech_step。
1 | server_step(State, ClientIn) -> |
Module:mech_step根据自身机制状态,返回不同的值。当验证通过时,返回ok信息。如若还需要其他信息继续验证,则返回continue信息。验证出错时,返回error信息。wait_for_feature_request函数根据不同的返回值,进行不同的处理。
- 当返回ok信息时,ejabberd_c2s进程向客户端发送验证成功的消息,登录验证流程结束。
- 当返回continue信息时,表示验证流程需要继续,因而向客户端返回服务端的chelange信息,ejabberd_c2s进程状态变为wait_for_sasl_response。当客户端再回应验证信息后,wait_for_sasl_response函数再次调用server_step进行验证处理。
- 当返回error信息时,ejabberd_c2s进程向客户端发送验证失败的消息,登录验证流程结束。
PLAIN机制只需要用户名和密码,这些信息附在客户端选择机制的AUTH请求中。因而只需要调用一次mech_step函数,mech_step也因而不会返回continue。cyrsasl_plain:mech_step调用mech_new中传入的check_password回调函数(不同机制会调用不同的回调函数)来检查用户名和密码是否正确。这部分逻辑由ejabberd_auth模块封装不同的模块完成,本文略过。
1 | mech_step(State, ClientIn) -> |
SCRAM机制需要多次Chelange/Response交互,需要多次调用它的mech_step。因而它在机制状态内部来分步骤完成,具体可以参考cyrsasl_scram.erl,本文不详述。
当机制mech_step返回ok或error时,ejabberd_c2s进程返回给客户端相应的回应,登录验证的流程结束。
注: ejabberd代码版本为2.1.13。