jabberd2源码分析:sasl认证部分

此篇为https://bluehua.org/2010/12/01/1484.html在jabberd2上的延伸。

jabberd2的sasl验证部分底层使用了第三方库,同时支持Cyrus和gsasl,configure时可以通过–with-sasl=指定一个。

认证发生在两种情况,客户端连接c2s组件,组件连接router。由于我不想再去分析一个客户端,所以c2s连接router为例分析认证过程。

先简单描述下jabberd2的启动过程:
jabberd2有四个可执行文件
router:路由器
c2s:组件,负责跟客户端建立连接,注册和验证帐号
sm:组件,会话管理器
s2s:组件,负责和其他xmpp服务器通信

router做为路由器首先启动,启动后会默认监听5347,等待包处理组件连接。

c2s,sm,s2s启动,并连接router,以c2s为例,分析一下往返的xml数据(C代表c2s做为客户端,S代表router做为服务端):
c2s跟router建立连接之后,大家互相寒暄一下,c2s说我吃了,router说他也吃了(互发stream头)

C:
S:

之后router给出自己支持的验证方式列表,说它只支持DIGEST—MD5

S:DIGEST-MD5

c2s回应说它接受DIGEST-MD5方式

C:

router给出challenge code

S:cmVhbG09ImphYmJlcmQtcm91dGVyIiwgbm9uY2U9Ii8xb0tDempxMXRNSUlLUHlOYTNCSHc9PSIsIHFvcD0iYXV0aCIsIGNoYXJzZXQ9dXRmLTgsIGFsZ29yaXRobT1tZDUtc2Vzcw==

c2s回应:

C:dXNlcm5hbWU9ImphYmJlcmQiLCByZWFsbT0iamFiYmVyZC1yb3V0ZXIiLCBub25jZT0iLzFvS0N6anExdE1JSUtQeU5hM0JIdz09IiwgY25vbmNlPSJueFZNempncTI1VUZtTjZmdUozdEZ3PT0iLCBuYz0wMDAwMDAwMSwgcW9wPWF1dGgsIGRpZ2VzdC11cmk9ImphYmJlcmQtcm91dGVyL2lwLTEwLTEzMC0xODctMTU1IiwgcmVzcG9uc2U9Yzg5NjJlNDgwZTVlYmM0ZjFkNjZiZDhlNDBiNjZlZjIsIGNoYXJzZXQ9dXRmLTg=

router再出一招

S:cnNwYXV0aD1hM2IyMGI0YzZkMzgxNmE3MzVjMWJhNTRjZTVkYmFjYQ==

c2s接

C:

router说验证通过

S:

验证通过之后的逻辑跟这里就无关了,需要注意的是验证通过之后这个stream并没有继续利用,c2s会发一个新的stream头,和router重新开始一个stream会话。

疑问:组件和router之间为什么要有验证机制?
虽然c2s,sm,s2s都在本地运行,但是如果一个远程进程可以连接到5347端口,那么它就可以向router注册。所以验证是必要的,所以5347端口的保护也是必要的。

下面是各个阶段在代码中的体现,主要从c2s的角度分析:
在这之前你必须首先理解jabberd2的io逻辑:https://bluehua.org/2010/11/28/1479.html(虽然老大表示文采太差,但是我觉得要是画图都不行,那我实在没辙了)

sasl验证逻辑主要位于:sx/sasl_gsasl.c sx/sasl_cyrus.c,这里以sasl_gsasl.c为例
先画一下sasl在jabber2中的位置

 +--------------------+
 |  callback(主要逻辑)|     -----
 +--------------------+     |
 |  SX(逻辑通道)      |     -----
 |  plugins     ------+-----+
 +--------------------+     +---- sasl
 |  callback          |     |
 +--------------------+     +----
 |  MIO(底层网络io)   |     |
 +--------------------+     |

sasl层以sx插件的形式被初始化,下面为c2s/main.c中注册sasl插件的部分,router中同理

/* get sasl online */
    c2s->sx_sasl = sx_env_plugin(c2s->sx_env, sx_sasl_init, "xmpp", _c2s_sx_sasl_callback, (void *) c2s);

看一下插件的初始化函数sasl_gsasl.c 906:int sx_sasl_init

//注册了以下几个回调函数
p->unload = _sx_sasl_unload;
//当数据送出时回调,貌似有的验证方法需要对送出的数据加密
p->wio = _sx_sasl_wio;
//当读入数据时回调,就是解密读入的数据
p->rio = _sx_sasl_rio;

//当一个strem建立时回调
p->stream = _sx_sasl_stream;
//当声明自己都有哪些特性时回调
p->features = _sx_sasl_features;
//当处理xml时回调
p->process = _sx_sasl_process;

下面描述一下c2s中的主要情形
….读取配置,加载插件
c2s/main.c 728行:
调用_c2s_router_connect初始化与router的sx通道
c2s/main.c 341行:
_c2s_router_connect的最后调用sx_client_init
sx/client.c 111行:
sx_client_init 构造一个stream头
sx/client.c 148行:
sx_client_init为即将写出的头加了一个回调,_sx_client_notify_header

sx写出strem头(具体怎么写还是看我画的那个图),并回调_sx_client_notify_header
sx/client.c 100行:
_sx_client_notify_header中为xml解析注册了两个回调
XML_SetElementHandler(s->expat, (void *) _sx_client_element_start, (void *) _sx_client_element_end);

mio层收到router回的stream头(具体怎么收到还是看我画的那个图…)
c2s/c2s.c 1308行:
回调到c2s_router_mio_callback
sx/io.c 181行:
c2s_router_mio_callback中调用sx_can_read
sx/io.c 24行:
sx_can_read调用_sx_process_read开始解收到的xml
解析起始标签时回调到_sx_client_element_start
sx/client.c 73行:
_sx_client_element_start解析到router给的stream头,改写xml解析回调
XML_SetElementHandler(s->expat, (void *) _sx_element_start, (void *) _sx_element_end);
sx/client.c 78行:
_sx_client_element_start 已经解析到router的strem头,strem建立,开始执行插件注册的strem回调
中间回调到sasl注册的process回调,_sx_sasl_process,但是不是它需要的包,没有动作.

由于sx_sasl_init前面注册了_sx_sasl_stream回调所以
_sx_sasl_stream用于在一个stream开始时确定是否已经做过sasl验证
sx/sasl_gsasl.c 312行 还没有gsasl session所以执行到316行便返回了

之后sx_can_read解完包触发event_PACKET,回调到c2s_router_sx_callback,没有任何动作

之后c2s收到router的stream:features包

DIGEST-MD5

经过mio又是层层回调,来到c2s_router_sx_callback
这回有动作了
c2s/c2s.c 787行:确定它收到了一个stream:features,调用sx_sasl_auth,初始化一个gsasl session
sx_sasl_auth(c2s->sx_sasl, s, “jabberd-router”, “DIGEST-MD5”, c2s->router_user, c2s->router_pass);
router_user和router_pass均从c2s.xml里读取
sx/sasl_gsasl.c 959行:sx_sasl_auth
初始化一个客户端gsasl session
ret = gsasl_client_start(ctx->gsasl_ctx, mech, &sd);
设置验证需要的数据
gsasl_session_hook_set(sd, (void *) ctx);
gsasl_property_set(sd, GSASL_AUTHID, user);
gsasl_property_set(sd, GSASL_PASSWORD, pass);
gsasl_property_set(sd, GSASL_SERVICE, appname);
gsasl_property_set(sd, GSASL_HOSTNAME, hostname);
//给插件回调传递句柄
s->plugin_data[p->index] = (void *) sd;

/* build the nad */
nad = nad_new();
ns = nad_add_namespace(nad, uri_SASL, NULL);

nad_append_elem(nad, ns, “auth”, 0);
nad_append_attr(nad, -1, “mechanism”, mech);
if(buf != NULL) {
nad_append_cdata(nad, buf, buflen, 1);
free(buf);
}

/* its away */
sx_nad_write(s, nad);
构造了一个


回应router

router回应

cmVhbG09ImphYmJlcmQtcm91dGVyIiwgbm9uY2U9Ii8xb0tDempxMXRNSUlLUHlOYTNCSHc9PSIsIHFvcD0iYXV0aCIsIGNoYXJzZXQ9dXRmLTgsIGFsZ29yaXRobT1tZDUtc2Vzcw==

c2s接受
c2s/c2s.c 1308行:
回调到c2s_router_mio_callback
sx/io.c 181行:
c2s_router_mio_callback中调用sx_can_read
sx/io.c 24行:
sx_can_read调用_sx_process_read开始解收到的xml
sx/io.c 142行:这次又回调到了_sx_sasl_process,这次有反应了,因为它匹配到xml中sasl的命名空间
sx/sasl_gsasl.c 700行:调用_sx_sasl_server_process
比较关键的一个函数,全贴出来吧
啥也不解释:看https://bluehua.org/2010/12/01/1484.html
唯一不同就是返回的结果的时候用_sx_sasl_response包成了xml

static void _sx_sasl_server_process(sx_t s, sx_plugin_t p, Gsasl_session *sd, char *in, int inlen) {
    char *buf = NULL, *out = NULL;
    size_t buflen, outlen;
    int ret;

    _sx_debug(ZONE, "data from client");

    /* decode the response */
    ret = gsasl_base64_from(in, inlen, &buf, &buflen);

    if (ret == GSASL_OK) {
        _sx_debug(ZONE, "decoded data: %.*s", buflen, buf);
    
        /* process the data */
        ret = gsasl_step(sd, buf, buflen, &out, &outlen);
        if(buf != NULL) free(buf); buf = NULL;
    
        /* in progress */
        if(ret == GSASL_OK || ret == GSASL_NEEDS_MORE) {
            _sx_debug(ZONE, "sasl handshake in progress (response: %.*s)", outlen, out);
    
            /* encode the response */
            ret = gsasl_base64_to(out, outlen, &buf, &buflen);

            if (ret == GSASL_OK) {
                _sx_nad_write(s, _sx_sasl_response(s, buf, buflen), 0);
            }

            if(out != NULL) free(out);
            if(buf != NULL) free(buf);
    
            return;
        }
    }
    if(out != NULL) free(out);
    if(buf != NULL) free(buf);

    /* its over */
    _sx_debug(ZONE, "sasl handshake aborted; (%d): %s", ret, gsasl_strerror(ret));

    _sx_nad_write(s, _sx_sasl_abort(s), 0);
}

sasl_gsasl.c 611行的_sx_nad_write之后,这次处理就算结束了
之后router继续给出challenge,c2s重复上面处理
最后router给出结果


重复上面,但是回调到_sx_sasl_process,处理方式就不一样了,调用了sx_client_init(s, flags, ns, to, from, version); 重新开启了一个stream

/* success */
else if(NAD_ENAME_L(nad, 0) == 7 && strncmp("success", NAD_ENAME(nad, 0), NAD_ENAME_L(nad, 0)) == 0) {
    _sx_debug(ZONE, "sasl handshake completed, resetting");
    nad_free(nad);

    /* save interesting bits */
    flags = s->flags;

    if(s->ns != NULL) ns = strdup(s->ns);

    if(s->req_to != NULL) to = strdup(s->req_to);
    if(s->req_from != NULL) from = strdup(s->req_from);
    if(s->req_version != NULL) version = strdup(s->req_version);

    /* reset state */
    _sx_reset(s);

    _sx_debug(ZONE, "restarting stream with sasl layer established");

    /* second time round */
    sx_client_init(s, flags, ns, to, from, version);

    /* free bits */
    if(ns != NULL) free(ns);
    if(to != NULL) free(to);
    if(from != NULL) free(from);
    if(version != NULL) free(version);

    return 0;
}

继续说下这个新起的stream把,它毫无疑问的回调到了sasl插件注册的_sx_sasl_stream,上次回调它什么都没做,是因为s->plugin_data[p->index]为NULL,但是前一个stream中在执行sx_sasl_auth时把一个gsasl seesion给了它,所以这次的逻辑就不一样了
sx/sasl_gsasl.c 326:它会继续调用_sx_sasl_open,来校验stream是否已经通过sasl验证.如果是它最终会执行
_sx_state(s, state_OPEN);
_sx_event(s, event_OPEN, NULL);
将这个通道的状态从state_STREAM变为 stat_OPEN,触发事件event_OPEN;

再看一下c2s_router_sx_callback中针对event_OPEN的处理吧

case event_OPEN:
    log_write(c2s->log, LOG_NOTICE, "connection to router established");

    /* set connection attempts counter */
    c2s->retry_left = c2s->retry_lost;

    nad = nad_new();
    ns = nad_add_namespace(nad, uri_COMPONENT, NULL);
    nad_append_elem(nad, ns, "bind", 0);
    nad_append_attr(nad, -1, "name", c2s->id);

    log_debug(ZONE, "requesting component bind for '%s'", c2s->id);

    sx_nad_write(c2s->router, nad);

    return 0;

开始一个绑定到router的bind请求!

OK,验证部分到此结束。。。,其他部分分析稍候

———–
post by gmail~

讨论

  1. Ethan

    想请教下, 你这个来回组件之间的数据沟通是如何得到的, 通过什么调试 ? 直接- D看LOG?

加入讨论

电子邮件地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据