文章标签 » c

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:<?xml version='1.0'?><stream:stream xmlns:stream='http://etherx.jabber.org/streams' version='1.0' xmlns:ack='http://www.xmpp.org/extensions/xep-0198.html#ns'>
S:<?xml version='1.0'?><stream:stream xmlns:stream='http://etherx.jabber.org/streams' version='1.0' id='yqvecsv64nooedcwo2mbt6f8x08krmlpch3oazve'>

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

S:<stream:features xmlns:stream='http://etherx.jabber.org/streams'><mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl'><mechanism>DIGEST-MD5</mechanism></mechanisms></stream:features>

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

C:<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='DIGEST-MD5'/>

router给出challenge code

S:<challenge xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>cmVhbG09ImphYmJlcmQtcm91dGVyIiwgbm9uY2U9Ii8xb0tDempxMXRNSUlLUHlOYTNCSHc9PSIsIHFvcD0iYXV0aCIsIGNoYXJzZXQ9dXRmLTgsIGFsZ29yaXRobT1tZDUtc2Vzcw==</challenge>

c2s回应:

C:<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>dXNlcm5hbWU9ImphYmJlcmQiLCByZWFsbT0iamFiYmVyZC1yb3V0ZXIiLCBub25jZT0iLzFvS0N6anExdE1JSUtQeU5hM0JIdz09IiwgY25vbmNlPSJueFZNempncTI1VUZtTjZmdUozdEZ3PT0iLCBuYz0wMDAwMDAwMSwgcW9wPWF1dGgsIGRpZ2VzdC11cmk9ImphYmJlcmQtcm91dGVyL2lwLTEwLTEzMC0xODctMTU1IiwgcmVzcG9uc2U9Yzg5NjJlNDgwZTVlYmM0ZjFkNjZiZDhlNDBiNjZlZjIsIGNoYXJzZXQ9dXRmLTg=</response>

router再出一招

S:<challenge xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>cnNwYXV0aD1hM2IyMGI0YzZkMzgxNmE3MzVjMWJhNTRjZTVkYmFjYQ==</challenge>

c2s接

C:<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/>

router说验证通过

S:<success xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/>

验证通过之后的逻辑跟这里就无关了,需要注意的是验证通过之后这个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中同理

706
707
/* 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包

<stream:features xmlns:stream='http://etherx.jabber.org/streams'><mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl'><mechanism>DIGEST-MD5</mechanism></mechanisms></stream:features>

经过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);
构造了一个

<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='DIGEST-MD5'/>

回应router

router回应

<challenge xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>cmVhbG09ImphYmJlcmQtcm91dGVyIiwgbm9uY2U9Ii8xb0tDempxMXRNSUlLUHlOYTNCSHc9PSIsIHFvcD0iYXV0aCIsIGNoYXJzZXQ9dXRmLTgsIGFsZ29yaXRobT1tZDUtc2Vzcw==</challenge>

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给出结果

<success xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/>

重复上面,但是回调到_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~

关于sasl

sasl(Simple Authentication and Security Layer)是一个用于网络通信协议的安全验证框架,它可以使网络协议在互相验证的阶段可以有多种验证方式可以选择。

现在实现sasl的协议主要有imap,smtp,pop,xmpp,subersion,….。举个smtp的例子说明一下sasl的验证流程:

1
2
3
4
5
6
7
8
250-mail.example.com Hello pc.example.org [192.168.1.42], pleased to meet you
250-AUTH DIGEST-MD5 CRAM-MD5 LOGIN PLAIN
AUTH LOGIN 
334 VXNlcm5hbWU6
eHh4eAo=
334 UGFzc3dvcmQ6
b294eAo=
235 2.0.0 OK Authenticated

1 和smtp sever建立连接后,server发送欢迎信息
2 server发送自己支持的验证方式DIGEST-MD5 CRAM-MD5 LOGIN PLAIN
3 client择使用LOGIN
4 server发送base64后的challenge code,base64解码后为Username:要求client回应一个用户名
5 client回应base64后的用户名
6 server发送base64后的challenge code,base64解码后为Password:要求client回应一个密码
7 client回应base64后的密码
8 server返回验证成功的信息

很简单的流程,服务端给出选择,客户端选一个,然后根据验证方式不同进行验证流程,由于sasl仅仅是个框架,具体怎么实现是由协议决定的,比如xmpp协议的sasl验证流程

#client与server建立链接后发出一个strem头
C:<stream:stream xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' to='example.com' version='1.0'>
#server回应一个steam头
S:<stream:stream xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' id='c2s_234' from='example.com' version='1.0'>
#server发送自己支持的验证方式列表
S:<stream:features>
     <mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>
       <mechanism>DIGEST-MD5</mechanism>
       <mechanism>PLAIN</mechanism>
     </mechanisms>
</stream:features>
#client 说它要用DIGEST-MD5做验证
C:
<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='DIGEST-MD5'/>
#server 发送challenge code
S:<challenge xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>
   cmVhbG09InNvbWVyZWFsbSIsbm9uY2U9Ik9BNk1HOXRFUUdtMmhoIixxb3A9ImF1dGgi
   LGNoYXJzZXQ9dXRmLTgsYWxnb3JpdGhtPW1kNS1zZXNzCg==
</challenge>
#client 回应challenge
C:
<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>
   dXNlcm5hbWU9InNvbWVub2RlIixyZWFsbT0ic29tZXJlYWxtIixub25jZT0i
   T0E2TUc5dEVRR20yaGgiLGNub25jZT0iT0E2TUhYaDZWcVRyUmsiLG5jPTAw
   MDAwMDAxLHFvcD1hdXRoLGRpZ2VzdC11cmk9InhtcHAvZXhhbXBsZS5jb20i
   LHJlc3BvbnNlPWQzODhkYWQ5MGQ0YmJkNzYwYTE1MjMyMWYyMTQzYWY3LGNo
   YXJzZXQ9dXRmLTgK
   </response>
#server 回应验证结果
S:
<success xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/>

当你实现一个协议的sasl部分时,如果你仅仅打算实现一两种验证方式,那么寥寥代码便可以搞定,但是如果希望提供尽可能多的验证方式,那么使用一些开源类库将是最好的选择。
对于C语言有两个成熟的lib:Cyrus SASL和libgsasl。以gsasl为例:

gsasl屏蔽了具体验证的细节,你要做的仅仅是为验证流程提供必要的信息,比如:用户名,密码,验证域等等
还是以上面的smtp验证为例,假设我们是client端,现在收到的server的mechlist,即验证方式列表,我们使用gsasl实现这次验证(虽然是我虚构的代码,但理论是可行的:)):

Gsasl *ctx = NULL;
char buffer[BUFSIZ] = "";
char *buf;
Gsasl_session *session;
int rc;
char *p;
//使用gsasl之前初始化
if ((rc = gsasl_init (&ctx)) != GSASL_OK)
{
    printf ("Cannot initialize libgsasl (%d): %s",
        rc, gsasl_strerror (rc));
    return;
}
 
//创建一个使用LOGIN验证方式的客户端session
if ((rc = gsasl_client_start (ctx, "LOGIN", &session)) != GSASL_OK)
{
    printf ("Cannot initialize client (%d): %s\n",
       rc, gsasl_strerror (rc));
    return;
}
 
//设置用户名和密码
gsasl_property_set (session, GSASL_AUTHID, "username");
gsasl_property_set (session, GSASL_PASSWORD, "password");
 
//假设socket_fd为我们与server已经建立连接的描述字
 
//告诉server我们选择使用LOGIN做验证
buf = buffer;
sprintf(buf, "AUTH LOGIN\r\n");
write(socket_fd, buf, strlen(buf));
 
do
 {
   buf = buffer;
   //从server读取一行,
   readline(buf, sizeof (buf) - 1, socket_fd);
   //334 是多余的 
   buf += 4;
   //将challenge code 交给gsasl处理
   rc = gsasl_step64 (session, buf, &p);
 
   if (rc == GSASL_NEEDS_MORE || rc == GSASL_OK)
     {
       //将gsasl的处理结果发送给server
       write(socket_fd, p, strlen(p));
       free (p);
     }
 }
while (rc == GSASL_NEEDS_MORE);
 
if (rc != GSASL_OK)
 {
   printf ("Authentication error (%d): %s\n",
           rc, gsasl_strerror (rc));
   return;
 }
 
printf("success!");
 
gsasl_finish (session);
gsasl_done (ctx);

使用gsasl可以让我们用类似上面代码处理所有的验证方式,唯一不同的就在于使用gsasl_property_set设置不同的字段。

除了像上面一样直接设置验证字段,还可以通过回调函数设置,当gsasl需要某一字段时会触发回调函数

int callback (Gsasl * ctx, Gsasl_session * sctx, Gsasl_property prop)
 {
   char buf[BUFSIZ] = "";
   int rc = GSASL_NO_CALLBACK;
 
   switch (prop)
     {
     case GSASL_AUTHID:
       gsasl_property_set (sctx, GSASL_AUTHID, "username");
       rc = GSASL_OK;
       break;
     // .............. 
     default:
       printf ("Unknown property!  Don't worry.\n");
       break;
     }
 
   return rc;
 }
//.................
gsasl_callback_set (ctx, callback);

好了,不能够再详细了,我的主要目的是分析jabberd2的验证逻辑,关于gsasl的更多请参考:http://www.gnu.org/software/gsasl/manual/gsasl.html

其他参考:http://wiki.jabbercn.org/index.php?title=RFC3920&variant=zh-cn

———–
post by gmail~

c arguments declare old style

看代码的时候看到这种类型声明

static void
add_shopt_to_alist (opt, on_or_off)
     char *opt;
     int on_or_off;
{
    // ...
}

立刻被震住了,smth上发贴弱问了一下,两分钟内便有两个回复,说这是一种老式的语法,也不好厚脸皮追问,顿时想出一串关键瓷:

c arguments declare old style

———–
post by gmail~

汉字转拼音的C语言版本

2011.11.23日更新
请移步到:http://code.google.com/p/bash-pinyin-completion/
更新了一个靠谱的拼音库
———————————–
2010.8.5日更新,svn版本12
修正了部分多音字拼音的优先级顺序
———————————–
…,本周接到用C的任务,搞定了一个生成头像的fastcgi程序,这个转拼音的写好了给别人用,我对C终于不是那么恐惧了~

生成可执行文件可以直接用,多音字会用|分开

$ echo 中国淫 | ./hz2py 
zhong guo yan|yao|yin

编译:

gcc -D HZ2PY_BINARY hz2py.c -o hz2py

源代码:http://code-of-emptyhua.googlecode.com/svn/trunk/chz2py/src/hz2py.c
可执行文件:http://code-of-emptyhua.googlecode.com/svn/trunk/bin/hz2py

———–
post by gmail~