source: protocols/jabber/iq.c @ 2334048

Last change on this file since 2334048 was 315dd4c, checked in by Wilmer van der Gaast <wilmer@…>, at 2010-03-15T01:25:47Z

Oops.. Today's Jabber fix could get stuck in a somewhat infinite loop if a
Jabber server returns an empty <iq type="result"/> response to the session
establishment request (which is valid and actually done by the example, but
my test Jabberd shows different behaviour). Fixed.

  • Property mode set to 100644
File size: 18.8 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       
311        if( jd->flags & JFLAG_WANT_BIND )
312        {
313                reply = xt_new_node( "bind", NULL, xt_new_node( "resource", set_getstr( &ic->acc->set, "resource" ), NULL ) );
314                xt_add_attr( reply, "xmlns", XMLNS_BIND );
315                jd->flags &= ~JFLAG_WANT_BIND;
316        }
317        else if( jd->flags & JFLAG_WANT_SESSION )
318        {
319                reply = xt_new_node( "session", NULL, NULL );
320                xt_add_attr( reply, "xmlns", XMLNS_SESSION );
321                jd->flags &= ~JFLAG_WANT_SESSION;
322        }
323       
324        if( reply != NULL )
325        {
326                reply = jabber_make_packet( "iq", "set", NULL, reply );
327                jabber_cache_add( ic, reply, jabber_pkt_bind_sess );
328               
329                if( !jabber_write_packet( ic, reply ) )
330                        return XT_ABORT;
331        }
332        else if( ( jd->flags & ( JFLAG_WANT_BIND | JFLAG_WANT_SESSION ) ) == 0 )
333        {
334                if( !jabber_get_roster( ic ) )
335                        return XT_ABORT;
336        }
337       
338        return XT_HANDLED;
339}
340
341int jabber_get_roster( struct im_connection *ic )
342{
343        struct xt_node *node;
344        int st;
345       
346        imcb_log( ic, "Authenticated, requesting buddy list" );
347       
348        node = xt_new_node( "query", NULL, NULL );
349        xt_add_attr( node, "xmlns", XMLNS_ROSTER );
350        node = jabber_make_packet( "iq", "get", NULL, node );
351       
352        jabber_cache_add( ic, node, jabber_parse_roster );
353        st = jabber_write_packet( ic, node );
354       
355        return st;
356}
357
358static xt_status jabber_parse_roster( struct im_connection *ic, struct xt_node *node, struct xt_node *orig )
359{
360        struct xt_node *query, *c;
361        int initial = ( orig != NULL );
362       
363        if( !( query = xt_find_node( node->children, "query" ) ) )
364        {
365                imcb_log( ic, "Warning: Received NULL roster packet" );
366                return XT_HANDLED;
367        }
368       
369        c = query->children;
370        while( ( c = xt_find_node( c, "item" ) ) )
371        {
372                struct xt_node *group = xt_find_node( node->children, "group" );
373                char *jid = xt_find_attr( c, "jid" );
374                char *name = xt_find_attr( c, "name" );
375                char *sub = xt_find_attr( c, "subscription" );
376               
377                if( jid && sub )
378                {
379                        if( ( strcmp( sub, "both" ) == 0 || strcmp( sub, "to" ) == 0 ) )
380                        {
381                                if( initial || imcb_find_buddy( ic, jid ) == NULL )
382                                        imcb_add_buddy( ic, jid, ( group && group->text_len ) ?
383                                                                   group->text : NULL );
384                               
385                                if( name )
386                                        imcb_rename_buddy( ic, jid, name );
387                        }
388                        else if( strcmp( sub, "remove" ) == 0 )
389                        {
390                                jabber_buddy_remove_bare( ic, jid );
391                                imcb_remove_buddy( ic, jid, NULL );
392                        }
393                }
394               
395                c = c->next;
396        }
397       
398        if( initial )
399                imcb_connected( ic );
400       
401        return XT_HANDLED;
402}
403
404int jabber_get_vcard( struct im_connection *ic, char *bare_jid )
405{
406        struct xt_node *node;
407       
408        if( strchr( bare_jid, '/' ) )
409                return 1;       /* This was an error, but return 0 should only be done if the connection died... */
410       
411        node = xt_new_node( "vCard", NULL, NULL );
412        xt_add_attr( node, "xmlns", XMLNS_VCARD );
413        node = jabber_make_packet( "iq", "get", bare_jid, node );
414       
415        jabber_cache_add( ic, node, jabber_iq_display_vcard );
416        return jabber_write_packet( ic, node );
417}
418
419static xt_status jabber_iq_display_vcard( struct im_connection *ic, struct xt_node *node, struct xt_node *orig )
420{
421        struct xt_node *vc, *c, *sc; /* subchild, ic is already in use ;-) */
422        GString *reply;
423        char *s;
424       
425        if( ( s = xt_find_attr( node, "type" ) ) == NULL ||
426            strcmp( s, "result" ) != 0 ||
427            ( vc = xt_find_node( node->children, "vCard" ) ) == NULL )
428        {
429                s = xt_find_attr( orig, "to" ); /* If this returns NULL something's wrong.. */
430                imcb_log( ic, "Could not retrieve vCard of %s", s ? s : "(NULL)" );
431                return XT_HANDLED;
432        }
433       
434        s = xt_find_attr( orig, "to" );
435        reply = g_string_new( "vCard information for " );
436        reply = g_string_append( reply, s ? s : "(NULL)" );
437        reply = g_string_append( reply, ":\n" );
438       
439        /* I hate this format, I really do... */
440       
441        if( ( c = xt_find_node( vc->children, "FN" ) ) && c->text_len )
442                g_string_append_printf( reply, "Name: %s\n", c->text );
443       
444        if( ( c = xt_find_node( vc->children, "N" ) ) && c->children )
445        {
446                reply = g_string_append( reply, "Full name:" );
447               
448                if( ( sc = xt_find_node( c->children, "PREFIX" ) ) && sc->text_len )
449                        g_string_append_printf( reply, " %s", sc->text );
450                if( ( sc = xt_find_node( c->children, "GIVEN" ) ) && sc->text_len )
451                        g_string_append_printf( reply, " %s", sc->text );
452                if( ( sc = xt_find_node( c->children, "MIDDLE" ) ) && sc->text_len )
453                        g_string_append_printf( reply, " %s", sc->text );
454                if( ( sc = xt_find_node( c->children, "FAMILY" ) ) && sc->text_len )
455                        g_string_append_printf( reply, " %s", sc->text );
456                if( ( sc = xt_find_node( c->children, "SUFFIX" ) ) && sc->text_len )
457                        g_string_append_printf( reply, " %s", sc->text );
458               
459                reply = g_string_append_c( reply, '\n' );
460        }
461       
462        if( ( c = xt_find_node( vc->children, "NICKNAME" ) ) && c->text_len )
463                g_string_append_printf( reply, "Nickname: %s\n", c->text );
464       
465        if( ( c = xt_find_node( vc->children, "BDAY" ) ) && c->text_len )
466                g_string_append_printf( reply, "Date of birth: %s\n", c->text );
467       
468        /* Slightly alternative use of for... ;-) */
469        for( c = vc->children; ( c = xt_find_node( c, "EMAIL" ) ); c = c->next )
470        {
471                if( ( sc = xt_find_node( c->children, "USERID" ) ) == NULL || sc->text_len == 0 )
472                        continue;
473               
474                if( xt_find_node( c->children, "HOME" ) )
475                        s = "Home";
476                else if( xt_find_node( c->children, "WORK" ) )
477                        s = "Work";
478                else
479                        s = "Misc.";
480               
481                g_string_append_printf( reply, "%s e-mail address: %s\n", s, sc->text );
482        }
483       
484        if( ( c = xt_find_node( vc->children, "URL" ) ) && c->text_len )
485                g_string_append_printf( reply, "Homepage: %s\n", c->text );
486       
487        /* Slightly alternative use of for... ;-) */
488        for( c = vc->children; ( c = xt_find_node( c, "ADR" ) ); c = c->next )
489        {
490                if( xt_find_node( c->children, "HOME" ) )
491                        s = "Home";
492                else if( xt_find_node( c->children, "WORK" ) )
493                        s = "Work";
494                else
495                        s = "Misc.";
496               
497                g_string_append_printf( reply, "%s address: ", s );
498               
499                if( ( sc = xt_find_node( c->children, "STREET" ) ) && sc->text_len )
500                        g_string_append_printf( reply, "%s ", sc->text );
501                if( ( sc = xt_find_node( c->children, "EXTADR" ) ) && sc->text_len )
502                        g_string_append_printf( reply, "%s, ", sc->text );
503                if( ( sc = xt_find_node( c->children, "PCODE" ) ) && sc->text_len )
504                        g_string_append_printf( reply, "%s, ", sc->text );
505                if( ( sc = xt_find_node( c->children, "LOCALITY" ) ) && sc->text_len )
506                        g_string_append_printf( reply, "%s, ", sc->text );
507                if( ( sc = xt_find_node( c->children, "REGION" ) ) && sc->text_len )
508                        g_string_append_printf( reply, "%s, ", sc->text );
509                if( ( sc = xt_find_node( c->children, "CTRY" ) ) && sc->text_len )
510                        g_string_append_printf( reply, "%s", sc->text );
511               
512                if( reply->str[reply->len-2] == ',' )
513                        reply = g_string_truncate( reply, reply->len-2 );
514               
515                reply = g_string_append_c( reply, '\n' );
516        }
517       
518        for( c = vc->children; ( c = xt_find_node( c, "TEL" ) ); c = c->next )
519        {
520                if( ( sc = xt_find_node( c->children, "NUMBER" ) ) == NULL || sc->text_len == 0 )
521                        continue;
522               
523                if( xt_find_node( c->children, "HOME" ) )
524                        s = "Home";
525                else if( xt_find_node( c->children, "WORK" ) )
526                        s = "Work";
527                else
528                        s = "Misc.";
529               
530                g_string_append_printf( reply, "%s phone number: %s\n", s, sc->text );
531        }
532       
533        if( ( c = xt_find_node( vc->children, "DESC" ) ) && c->text_len )
534                g_string_append_printf( reply, "Other information:\n%s", c->text );
535       
536        /* *sigh* */
537       
538        imcb_log( ic, "%s", reply->str );
539        g_string_free( reply, TRUE );
540       
541        return XT_HANDLED;
542}
543
544static xt_status jabber_add_to_roster_callback( struct im_connection *ic, struct xt_node *node, struct xt_node *orig );
545
546int jabber_add_to_roster( struct im_connection *ic, char *handle, char *name )
547{
548        struct xt_node *node;
549        int st;
550       
551        /* Build the item entry */
552        node = xt_new_node( "item", NULL, NULL );
553        xt_add_attr( node, "jid", handle );
554        if( name )
555                xt_add_attr( node, "name", name );
556       
557        /* And pack it into a roster-add packet */
558        node = xt_new_node( "query", NULL, node );
559        xt_add_attr( node, "xmlns", XMLNS_ROSTER );
560        node = jabber_make_packet( "iq", "set", NULL, node );
561        jabber_cache_add( ic, node, jabber_add_to_roster_callback );
562       
563        st = jabber_write_packet( ic, node );
564       
565        return st;
566}
567
568static xt_status jabber_add_to_roster_callback( struct im_connection *ic, struct xt_node *node, struct xt_node *orig )
569{
570        char *s, *jid = NULL;
571        struct xt_node *c;
572       
573        if( ( c = xt_find_node( orig->children, "query" ) ) &&
574            ( c = xt_find_node( c->children, "item" ) ) &&
575            ( jid = xt_find_attr( c, "jid" ) ) &&
576            ( s = xt_find_attr( node, "type" ) ) &&
577            strcmp( s, "result" ) == 0 )
578        {
579                if( imcb_find_buddy( ic, jid ) == NULL )
580                        imcb_add_buddy( ic, jid, NULL );
581        }
582        else
583        {
584                imcb_log( ic, "Error while adding `%s' to your contact list.",
585                          jid ? jid : "(unknown handle)" );
586        }
587       
588        return XT_HANDLED;
589}
590
591int jabber_remove_from_roster( struct im_connection *ic, char *handle )
592{
593        struct xt_node *node;
594        int st;
595       
596        /* Build the item entry */
597        node = xt_new_node( "item", NULL, NULL );
598        xt_add_attr( node, "jid", handle );
599        xt_add_attr( node, "subscription", "remove" );
600       
601        /* And pack it into a roster-add packet */
602        node = xt_new_node( "query", NULL, node );
603        xt_add_attr( node, "xmlns", XMLNS_ROSTER );
604        node = jabber_make_packet( "iq", "set", NULL, node );
605       
606        st = jabber_write_packet( ic, node );
607       
608        xt_free_node( node );
609        return st;
610}
Note: See TracBrowser for help on using the repository browser.