source: protocols/jabber/iq.c @ 8fb1263

Last change on this file since 8fb1263 was 8fb1263, checked in by Wilmer van der Gaast <wilmer@…>, at 2010-03-14T17:45:33Z

Don't send bind and session requests at the same time when logging in
because some very picky jabberd's don't like it. (Fixes Bug #569)

  • Property mode set to 100644
File size: 18.9 KB
Line 
1/***************************************************************************\
2*                                                                           *
3*  BitlBee - An IRC to IM gateway                                           *
4*  Jabber module - IQ packets                                               *
5*                                                                           *
6*  Copyright 2006 Wilmer van der Gaast <wilmer@gaast.net>                   *
7*                                                                           *
8*  This program is free software; you can redistribute it and/or modify     *
9*  it under the terms of the GNU General Public License as published by     *
10*  the Free Software Foundation; either version 2 of the License, or        *
11*  (at your option) any later version.                                      *
12*                                                                           *
13*  This program is distributed in the hope that it will be useful,          *
14*  but WITHOUT ANY WARRANTY; without even the implied warranty of           *
15*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the            *
16*  GNU General Public License for more details.                             *
17*                                                                           *
18*  You should have received a copy of the GNU General Public License along  *
19*  with this program; if not, write to the Free Software Foundation, Inc.,  *
20*  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.              *
21*                                                                           *
22\***************************************************************************/
23
24#include "jabber.h"
25#include "sha1.h"
26
27static xt_status jabber_parse_roster( struct im_connection *ic, struct xt_node *node, struct xt_node *orig );
28static xt_status jabber_iq_display_vcard( struct im_connection *ic, struct xt_node *node, struct xt_node *orig );
29
30xt_status jabber_pkt_iq( struct xt_node *node, gpointer data )
31{
32        struct im_connection *ic = data;
33        struct xt_node *c, *reply = NULL;
34        char *type, *s;
35        int st, pack = 1;
36       
37        type = xt_find_attr( node, "type" );
38       
39        if( !type )
40        {
41                imcb_error( ic, "Received IQ packet without type." );
42                imc_logout( ic, TRUE );
43                return XT_ABORT;
44        }
45       
46        if( strcmp( type, "result" ) == 0 || strcmp( type, "error" ) == 0 )
47        {
48                return jabber_cache_handle_packet( ic, node );
49        }
50        else if( strcmp( type, "get" ) == 0 )
51        {
52                if( !( ( c = xt_find_node( node->children, "query" ) ) ||
53                       ( c = xt_find_node( node->children, "ping" ) ) ) ||
54                    !( s = xt_find_attr( c, "xmlns" ) ) )
55                {
56                        /* Sigh. Who decided to suddenly invent new elements
57                           instead of just sticking with <query/>? */
58                        return XT_HANDLED;
59                }
60               
61                reply = xt_new_node( "query", NULL, NULL );
62                xt_add_attr( reply, "xmlns", s );
63               
64                /* Of course this is a very essential query to support. ;-) */
65                if( strcmp( s, XMLNS_VERSION ) == 0 )
66                {
67                        xt_add_child( reply, xt_new_node( "name", "BitlBee", NULL ) );
68                        xt_add_child( reply, xt_new_node( "version", BITLBEE_VERSION, NULL ) );
69                        xt_add_child( reply, xt_new_node( "os", ARCH, NULL ) );
70                }
71                else if( strcmp( s, XMLNS_TIME ) == 0 )
72                {
73                        time_t time_ep;
74                        char buf[1024];
75                       
76                        buf[sizeof(buf)-1] = 0;
77                        time_ep = time( NULL );
78                       
79                        strftime( buf, sizeof( buf ) - 1, "%Y%m%dT%H:%M:%S", gmtime( &time_ep ) );
80                        xt_add_child( reply, xt_new_node( "utc", buf, NULL ) );
81                       
82                        strftime( buf, sizeof( buf ) - 1, "%Z", localtime( &time_ep ) );
83                        xt_add_child( reply, xt_new_node( "tz", buf, NULL ) );
84                }
85                else if( strcmp( s, XMLNS_PING ) == 0 )
86                {
87                        xt_free_node( reply );
88                        reply = jabber_make_packet( "iq", "result", xt_find_attr( node, "from" ), NULL );
89                        if( ( s = xt_find_attr( node, "id" ) ) )
90                                xt_add_attr( reply, "id", s );
91                        pack = 0;
92                }
93                else if( strcmp( s, XMLNS_DISCOVER ) == 0 )
94                {
95                        const char *features[] = { XMLNS_DISCOVER,
96                                                   XMLNS_VERSION,
97                                                   XMLNS_TIME,
98                                                   XMLNS_CHATSTATES,
99                                                   XMLNS_MUC,
100                                                   XMLNS_PING,
101                                                   NULL };
102                        const char **f;
103                       
104                        c = xt_new_node( "identity", NULL, NULL );
105                        xt_add_attr( c, "category", "client" );
106                        xt_add_attr( c, "type", "pc" );
107                        xt_add_attr( c, "name", "BitlBee" );
108                        xt_add_child( reply, c );
109                       
110                        for( f = features; *f; f ++ )
111                        {
112                                c = xt_new_node( "feature", NULL, NULL );
113                                xt_add_attr( c, "var", *f );
114                                xt_add_child( reply, c );
115                        }
116                }
117                else
118                {
119                        xt_free_node( reply );
120                        reply = jabber_make_error_packet( node, "feature-not-implemented", "cancel" );
121                        pack = 0;
122                }
123        }
124        else if( strcmp( type, "set" ) == 0 )
125        {
126                if( !( c = xt_find_node( node->children, "query" ) ) ||
127                    !( s = xt_find_attr( c, "xmlns" ) ) )
128                {
129                        imcb_log( ic, "Warning: Received incomplete IQ-%s packet", type );
130                        return XT_HANDLED;
131                }
132               
133                /* This is a roster push. XMPP servers send this when someone
134                   was added to (or removed from) the buddy list. AFAIK they're
135                   sent even if we added this buddy in our own session. */
136                if( strcmp( s, XMLNS_ROSTER ) == 0 )
137                {
138                        int bare_len = strlen( ic->acc->user );
139                       
140                        if( ( s = xt_find_attr( node, "from" ) ) == NULL ||
141                            ( strncmp( s, ic->acc->user, bare_len ) == 0 &&
142                              ( s[bare_len] == 0 || s[bare_len] == '/' ) ) )
143                        {
144                                jabber_parse_roster( ic, node, NULL );
145                               
146                                /* Should we generate a reply here? Don't think it's
147                                   very important... */
148                        }
149                        else
150                        {
151                                imcb_log( ic, "Warning: %s tried to fake a roster push!", s ? s : "(unknown)" );
152                               
153                                xt_free_node( reply );
154                                reply = jabber_make_error_packet( node, "not-allowed", "cancel" );
155                                pack = 0;
156                        }
157                }
158                else
159                {
160                        xt_free_node( reply );
161                        reply = jabber_make_error_packet( node, "feature-not-implemented", "cancel" );
162                        pack = 0;
163                }
164        }
165       
166        /* If we recognized the xmlns and managed to generate a reply,
167           finish and send it. */
168        if( reply )
169        {
170                /* Normally we still have to pack it into an iq-result
171                   packet, but for errors, for example, we don't. */
172                if( pack )
173                {
174                        reply = jabber_make_packet( "iq", "result", xt_find_attr( node, "from" ), reply );
175                        if( ( s = xt_find_attr( node, "id" ) ) )
176                                xt_add_attr( reply, "id", s );
177                }
178               
179                st = jabber_write_packet( ic, reply );
180                xt_free_node( reply );
181                if( !st )
182                        return XT_ABORT;
183        }
184       
185        return XT_HANDLED;
186}
187
188static xt_status jabber_do_iq_auth( struct im_connection *ic, struct xt_node *node, struct xt_node *orig );
189static xt_status jabber_finish_iq_auth( struct im_connection *ic, struct xt_node *node, struct xt_node *orig );
190
191int jabber_init_iq_auth( struct im_connection *ic )
192{
193        struct jabber_data *jd = ic->proto_data;
194        struct xt_node *node;
195        int st;
196       
197        node = xt_new_node( "query", NULL, xt_new_node( "username", jd->username, NULL ) );
198        xt_add_attr( node, "xmlns", XMLNS_AUTH );
199        node = jabber_make_packet( "iq", "get", NULL, node );
200       
201        jabber_cache_add( ic, node, jabber_do_iq_auth );
202        st = jabber_write_packet( ic, node );
203       
204        return st;
205}
206
207static xt_status jabber_do_iq_auth( struct im_connection *ic, struct xt_node *node, struct xt_node *orig )
208{
209        struct jabber_data *jd = ic->proto_data;
210        struct xt_node *reply, *query;
211        xt_status st;
212        char *s;
213       
214        if( !( query = xt_find_node( node->children, "query" ) ) )
215        {
216                imcb_log( ic, "Warning: Received incomplete IQ packet while authenticating" );
217                imc_logout( ic, FALSE );
218                return XT_HANDLED;
219        }
220       
221        /* Time to authenticate ourselves! */
222        reply = xt_new_node( "query", NULL, NULL );
223        xt_add_attr( reply, "xmlns", XMLNS_AUTH );
224        xt_add_child( reply, xt_new_node( "username", jd->username, NULL ) );
225        xt_add_child( reply, xt_new_node( "resource", set_getstr( &ic->acc->set, "resource" ), NULL ) );
226       
227        if( xt_find_node( query->children, "digest" ) && ( s = xt_find_attr( jd->xt->root, "id" ) ) )
228        {
229                /* We can do digest authentication, it seems, and of
230                   course we prefer that. */
231                sha1_state_t sha;
232                char hash_hex[41];
233                unsigned char hash[20];
234                int i;
235               
236                sha1_init( &sha );
237                sha1_append( &sha, (unsigned char*) s, strlen( s ) );
238                sha1_append( &sha, (unsigned char*) ic->acc->pass, strlen( ic->acc->pass ) );
239                sha1_finish( &sha, hash );
240               
241                for( i = 0; i < 20; i ++ )
242                        sprintf( hash_hex + i * 2, "%02x", hash[i] );
243               
244                xt_add_child( reply, xt_new_node( "digest", hash_hex, NULL ) );
245        }
246        else if( xt_find_node( query->children, "password" ) )
247        {
248                /* We'll have to stick with plaintext. Let's hope we're using SSL/TLS... */
249                xt_add_child( reply, xt_new_node( "password", ic->acc->pass, NULL ) );
250        }
251        else
252        {
253                xt_free_node( reply );
254               
255                imcb_error( ic, "Can't find suitable authentication method" );
256                imc_logout( ic, FALSE );
257                return XT_ABORT;
258        }
259       
260        reply = jabber_make_packet( "iq", "set", NULL, reply );
261        jabber_cache_add( ic, reply, jabber_finish_iq_auth );
262        st = jabber_write_packet( ic, reply );
263       
264        return st ? XT_HANDLED : XT_ABORT;
265}
266
267static xt_status jabber_finish_iq_auth( struct im_connection *ic, struct xt_node *node, struct xt_node *orig )
268{
269        struct jabber_data *jd = ic->proto_data;
270        char *type;
271       
272        if( !( type = xt_find_attr( node, "type" ) ) )
273        {
274                imcb_log( ic, "Warning: Received incomplete IQ packet while authenticating" );
275                imc_logout( ic, FALSE );
276                return XT_HANDLED;
277        }
278       
279        if( strcmp( type, "error" ) == 0 )
280        {
281                imcb_error( ic, "Authentication failure" );
282                imc_logout( ic, FALSE );
283                return XT_ABORT;
284        }
285        else if( strcmp( type, "result" ) == 0 )
286        {
287                /* This happens when we just successfully authenticated the
288                   old (non-SASL) way. */
289                jd->flags |= JFLAG_AUTHENTICATED;
290                if( !jabber_get_roster( ic ) )
291                        return XT_ABORT;
292        }
293       
294        return XT_HANDLED;
295}
296
297xt_status jabber_pkt_bind_sess( struct im_connection *ic, struct xt_node *node, struct xt_node *orig )
298{
299        struct jabber_data *jd = ic->proto_data;
300        struct xt_node *c, *reply = NULL;
301        char *s;
302       
303        if( node && ( c = xt_find_node( node->children, "bind" ) ) )
304        {
305                c = xt_find_node( c->children, "jid" );
306                if( c && c->text_len && ( s = strchr( c->text, '/' ) ) &&
307                    strcmp( s + 1, set_getstr( &ic->acc->set, "resource" ) ) != 0 )
308                        imcb_log( ic, "Server changed session resource string to `%s'", s + 1 );
309               
310                jd->flags &= ~JFLAG_WANT_BIND;
311        }
312        else if( node && ( c = xt_find_node( node->children, "session" ) ) )
313        {
314                jd->flags &= ~JFLAG_WANT_SESSION;
315        }
316       
317        if( jd->flags & JFLAG_WANT_BIND )
318        {
319                reply = xt_new_node( "bind", NULL, xt_new_node( "resource", set_getstr( &ic->acc->set, "resource" ), NULL ) );
320                xt_add_attr( reply, "xmlns", XMLNS_BIND );
321        }
322        else if( jd->flags & JFLAG_WANT_SESSION )
323        {
324                reply = xt_new_node( "session", NULL, NULL );
325                xt_add_attr( reply, "xmlns", XMLNS_SESSION );
326        }
327       
328        if( reply != NULL )
329        {
330                reply = jabber_make_packet( "iq", "set", NULL, reply );
331                jabber_cache_add( ic, reply, jabber_pkt_bind_sess );
332               
333                if( !jabber_write_packet( ic, reply ) )
334                        return XT_ABORT;
335        }
336        else if( ( jd->flags & ( JFLAG_WANT_BIND | JFLAG_WANT_SESSION ) ) == 0 )
337        {
338                if( !jabber_get_roster( ic ) )
339                        return XT_ABORT;
340        }
341       
342        return XT_HANDLED;
343}
344
345int jabber_get_roster( struct im_connection *ic )
346{
347        struct xt_node *node;
348        int st;
349       
350        imcb_log( ic, "Authenticated, requesting buddy list" );
351       
352        node = xt_new_node( "query", NULL, NULL );
353        xt_add_attr( node, "xmlns", XMLNS_ROSTER );
354        node = jabber_make_packet( "iq", "get", NULL, node );
355       
356        jabber_cache_add( ic, node, jabber_parse_roster );
357        st = jabber_write_packet( ic, node );
358       
359        return st;
360}
361
362static xt_status jabber_parse_roster( struct im_connection *ic, struct xt_node *node, struct xt_node *orig )
363{
364        struct xt_node *query, *c;
365        int initial = ( orig != NULL );
366       
367        if( !( query = xt_find_node( node->children, "query" ) ) )
368        {
369                imcb_log( ic, "Warning: Received NULL roster packet" );
370                return XT_HANDLED;
371        }
372       
373        c = query->children;
374        while( ( c = xt_find_node( c, "item" ) ) )
375        {
376                struct xt_node *group = xt_find_node( node->children, "group" );
377                char *jid = xt_find_attr( c, "jid" );
378                char *name = xt_find_attr( c, "name" );
379                char *sub = xt_find_attr( c, "subscription" );
380               
381                if( jid && sub )
382                {
383                        if( ( strcmp( sub, "both" ) == 0 || strcmp( sub, "to" ) == 0 ) )
384                        {
385                                if( initial || imcb_find_buddy( ic, jid ) == NULL )
386                                        imcb_add_buddy( ic, jid, ( group && group->text_len ) ?
387                                                                   group->text : NULL );
388                               
389                                if( name )
390                                        imcb_rename_buddy( ic, jid, name );
391                        }
392                        else if( strcmp( sub, "remove" ) == 0 )
393                        {
394                                jabber_buddy_remove_bare( ic, jid );
395                                imcb_remove_buddy( ic, jid, NULL );
396                        }
397                }
398               
399                c = c->next;
400        }
401       
402        if( initial )
403                imcb_connected( ic );
404       
405        return XT_HANDLED;
406}
407
408int jabber_get_vcard( struct im_connection *ic, char *bare_jid )
409{
410        struct xt_node *node;
411       
412        if( strchr( bare_jid, '/' ) )
413                return 1;       /* This was an error, but return 0 should only be done if the connection died... */
414       
415        node = xt_new_node( "vCard", NULL, NULL );
416        xt_add_attr( node, "xmlns", XMLNS_VCARD );
417        node = jabber_make_packet( "iq", "get", bare_jid, node );
418       
419        jabber_cache_add( ic, node, jabber_iq_display_vcard );
420        return jabber_write_packet( ic, node );
421}
422
423static xt_status jabber_iq_display_vcard( struct im_connection *ic, struct xt_node *node, struct xt_node *orig )
424{
425        struct xt_node *vc, *c, *sc; /* subchild, ic is already in use ;-) */
426        GString *reply;
427        char *s;
428       
429        if( ( s = xt_find_attr( node, "type" ) ) == NULL ||
430            strcmp( s, "result" ) != 0 ||
431            ( vc = xt_find_node( node->children, "vCard" ) ) == NULL )
432        {
433                s = xt_find_attr( orig, "to" ); /* If this returns NULL something's wrong.. */
434                imcb_log( ic, "Could not retrieve vCard of %s", s ? s : "(NULL)" );
435                return XT_HANDLED;
436        }
437       
438        s = xt_find_attr( orig, "to" );
439        reply = g_string_new( "vCard information for " );
440        reply = g_string_append( reply, s ? s : "(NULL)" );
441        reply = g_string_append( reply, ":\n" );
442       
443        /* I hate this format, I really do... */
444       
445        if( ( c = xt_find_node( vc->children, "FN" ) ) && c->text_len )
446                g_string_append_printf( reply, "Name: %s\n", c->text );
447       
448        if( ( c = xt_find_node( vc->children, "N" ) ) && c->children )
449        {
450                reply = g_string_append( reply, "Full name:" );
451               
452                if( ( sc = xt_find_node( c->children, "PREFIX" ) ) && sc->text_len )
453                        g_string_append_printf( reply, " %s", sc->text );
454                if( ( sc = xt_find_node( c->children, "GIVEN" ) ) && sc->text_len )
455                        g_string_append_printf( reply, " %s", sc->text );
456                if( ( sc = xt_find_node( c->children, "MIDDLE" ) ) && sc->text_len )
457                        g_string_append_printf( reply, " %s", sc->text );
458                if( ( sc = xt_find_node( c->children, "FAMILY" ) ) && sc->text_len )
459                        g_string_append_printf( reply, " %s", sc->text );
460                if( ( sc = xt_find_node( c->children, "SUFFIX" ) ) && sc->text_len )
461                        g_string_append_printf( reply, " %s", sc->text );
462               
463                reply = g_string_append_c( reply, '\n' );
464        }
465       
466        if( ( c = xt_find_node( vc->children, "NICKNAME" ) ) && c->text_len )
467                g_string_append_printf( reply, "Nickname: %s\n", c->text );
468       
469        if( ( c = xt_find_node( vc->children, "BDAY" ) ) && c->text_len )
470                g_string_append_printf( reply, "Date of birth: %s\n", c->text );
471       
472        /* Slightly alternative use of for... ;-) */
473        for( c = vc->children; ( c = xt_find_node( c, "EMAIL" ) ); c = c->next )
474        {
475                if( ( sc = xt_find_node( c->children, "USERID" ) ) == NULL || sc->text_len == 0 )
476                        continue;
477               
478                if( xt_find_node( c->children, "HOME" ) )
479                        s = "Home";
480                else if( xt_find_node( c->children, "WORK" ) )
481                        s = "Work";
482                else
483                        s = "Misc.";
484               
485                g_string_append_printf( reply, "%s e-mail address: %s\n", s, sc->text );
486        }
487       
488        if( ( c = xt_find_node( vc->children, "URL" ) ) && c->text_len )
489                g_string_append_printf( reply, "Homepage: %s\n", c->text );
490       
491        /* Slightly alternative use of for... ;-) */
492        for( c = vc->children; ( c = xt_find_node( c, "ADR" ) ); c = c->next )
493        {
494                if( xt_find_node( c->children, "HOME" ) )
495                        s = "Home";
496                else if( xt_find_node( c->children, "WORK" ) )
497                        s = "Work";
498                else
499                        s = "Misc.";
500               
501                g_string_append_printf( reply, "%s address: ", s );
502               
503                if( ( sc = xt_find_node( c->children, "STREET" ) ) && sc->text_len )
504                        g_string_append_printf( reply, "%s ", sc->text );
505                if( ( sc = xt_find_node( c->children, "EXTADR" ) ) && sc->text_len )
506                        g_string_append_printf( reply, "%s, ", sc->text );
507                if( ( sc = xt_find_node( c->children, "PCODE" ) ) && sc->text_len )
508                        g_string_append_printf( reply, "%s, ", sc->text );
509                if( ( sc = xt_find_node( c->children, "LOCALITY" ) ) && sc->text_len )
510                        g_string_append_printf( reply, "%s, ", sc->text );
511                if( ( sc = xt_find_node( c->children, "REGION" ) ) && sc->text_len )
512                        g_string_append_printf( reply, "%s, ", sc->text );
513                if( ( sc = xt_find_node( c->children, "CTRY" ) ) && sc->text_len )
514                        g_string_append_printf( reply, "%s", sc->text );
515               
516                if( reply->str[reply->len-2] == ',' )
517                        reply = g_string_truncate( reply, reply->len-2 );
518               
519                reply = g_string_append_c( reply, '\n' );
520        }
521       
522        for( c = vc->children; ( c = xt_find_node( c, "TEL" ) ); c = c->next )
523        {
524                if( ( sc = xt_find_node( c->children, "NUMBER" ) ) == NULL || sc->text_len == 0 )
525                        continue;
526               
527                if( xt_find_node( c->children, "HOME" ) )
528                        s = "Home";
529                else if( xt_find_node( c->children, "WORK" ) )
530                        s = "Work";
531                else
532                        s = "Misc.";
533               
534                g_string_append_printf( reply, "%s phone number: %s\n", s, sc->text );
535        }
536       
537        if( ( c = xt_find_node( vc->children, "DESC" ) ) && c->text_len )
538                g_string_append_printf( reply, "Other information:\n%s", c->text );
539       
540        /* *sigh* */
541       
542        imcb_log( ic, "%s", reply->str );
543        g_string_free( reply, TRUE );
544       
545        return XT_HANDLED;
546}
547
548static xt_status jabber_add_to_roster_callback( struct im_connection *ic, struct xt_node *node, struct xt_node *orig );
549
550int jabber_add_to_roster( struct im_connection *ic, char *handle, char *name )
551{
552        struct xt_node *node;
553        int st;
554       
555        /* Build the item entry */
556        node = xt_new_node( "item", NULL, NULL );
557        xt_add_attr( node, "jid", handle );
558        if( name )
559                xt_add_attr( node, "name", name );
560       
561        /* And pack it into a roster-add packet */
562        node = xt_new_node( "query", NULL, node );
563        xt_add_attr( node, "xmlns", XMLNS_ROSTER );
564        node = jabber_make_packet( "iq", "set", NULL, node );
565        jabber_cache_add( ic, node, jabber_add_to_roster_callback );
566       
567        st = jabber_write_packet( ic, node );
568       
569        return st;
570}
571
572static xt_status jabber_add_to_roster_callback( struct im_connection *ic, struct xt_node *node, struct xt_node *orig )
573{
574        char *s, *jid = NULL;
575        struct xt_node *c;
576       
577        if( ( c = xt_find_node( orig->children, "query" ) ) &&
578            ( c = xt_find_node( c->children, "item" ) ) &&
579            ( jid = xt_find_attr( c, "jid" ) ) &&
580            ( s = xt_find_attr( node, "type" ) ) &&
581            strcmp( s, "result" ) == 0 )
582        {
583                if( imcb_find_buddy( ic, jid ) == NULL )
584                        imcb_add_buddy( ic, jid, NULL );
585        }
586        else
587        {
588                imcb_log( ic, "Error while adding `%s' to your contact list.",
589                          jid ? jid : "(unknown handle)" );
590        }
591       
592        return XT_HANDLED;
593}
594
595int jabber_remove_from_roster( struct im_connection *ic, char *handle )
596{
597        struct xt_node *node;
598        int st;
599       
600        /* Build the item entry */
601        node = xt_new_node( "item", NULL, NULL );
602        xt_add_attr( node, "jid", handle );
603        xt_add_attr( node, "subscription", "remove" );
604       
605        /* And pack it into a roster-add packet */
606        node = xt_new_node( "query", NULL, node );
607        xt_add_attr( node, "xmlns", XMLNS_ROSTER );
608        node = jabber_make_packet( "iq", "set", NULL, node );
609       
610        st = jabber_write_packet( ic, node );
611       
612        xt_free_node( node );
613        return st;
614}
Note: See TracBrowser for help on using the repository browser.