source: libfaim/auth.c @ 4789b17

barnowl_perlaimdebianowlrelease-1.10release-1.4release-1.5release-1.6release-1.7release-1.8release-1.9
Last change on this file since 4789b17 was cf02dd6, checked in by James M. Kretchmar <kretch@mit.edu>, 21 years ago
*** empty log message ***
  • Property mode set to 100644
File size: 15.6 KB
RevLine 
[5e53c4a]1/*
[862371b]2 * Family 0x0017 - Authentication.
3 *
4 * Deals with the authorizer for SNAC-based login, and also old-style
5 * non-SNAC login.
[5e53c4a]6 *
7 */
8
9#define FAIM_INTERNAL
[e374dee]10#include <aim.h>
[5e53c4a]11
12#include "md5.h"
13
[cf02dd6]14#include <ctype.h>
[5e53c4a]15
[cf02dd6]16/**
17 * Encode a password using old XOR method
[5e53c4a]18 *
[cf02dd6]19 * This takes a const pointer to a (null terminated) string
20 * containing the unencoded password.  It also gets passed
21 * an already allocated buffer to store the encoded password.
22 * This buffer should be the exact length of the password without
23 * the null.  The encoded password buffer /is not %NULL terminated/.
[5e53c4a]24 *
[cf02dd6]25 * The encoding_table seems to be a fixed set of values.  We'll
26 * hope it doesn't change over time! 
27 *
28 * This is only used for the XOR method, not the better MD5 method.
29 *
30 * @param password Incoming password.
31 * @param encoded Buffer to put encoded password.
[5e53c4a]32 */
[cf02dd6]33static int aim_encode_password(const char *password, fu8_t *encoded)
[5e53c4a]34{
[cf02dd6]35        fu8_t encoding_table[] = {
36#if 0 /* old v1 table */
37                0xf3, 0xb3, 0x6c, 0x99,
38                0x95, 0x3f, 0xac, 0xb6,
39                0xc5, 0xfa, 0x6b, 0x63,
40                0x69, 0x6c, 0xc3, 0x9f
41#else /* v2.1 table, also works for ICQ */
42                0xf3, 0x26, 0x81, 0xc4,
43                0x39, 0x86, 0xdb, 0x92,
44                0x71, 0xa3, 0xb9, 0xe6,
45                0x53, 0x7a, 0x95, 0x7c
46#endif
47        };
48        int i;
[5e53c4a]49
[cf02dd6]50        for (i = 0; i < strlen(password); i++)
51                encoded[i] = (password[i] ^ encoding_table[i]);
[5e53c4a]52
[cf02dd6]53        return 0;
54}
[5e53c4a]55
[cf02dd6]56#ifdef USE_OLD_MD5
57static int aim_encode_password_md5(const char *password, const char *key, fu8_t *digest)
58{
59        md5_state_t state;
60
61        md5_init(&state);       
62        md5_append(&state, (const md5_byte_t *)key, strlen(key));
63        md5_append(&state, (const md5_byte_t *)password, strlen(password));
64        md5_append(&state, (const md5_byte_t *)AIM_MD5_STRING, strlen(AIM_MD5_STRING));
65        md5_finish(&state, (md5_byte_t *)digest);
66
67        return 0;
68}
69#else
70static int aim_encode_password_md5(const char *password, const char *key, fu8_t *digest)
71{
72        md5_state_t state;
73        fu8_t passdigest[16];
74
75        md5_init(&state);
76        md5_append(&state, (const md5_byte_t *)password, strlen(password));
77        md5_finish(&state, (md5_byte_t *)&passdigest);
78
79        md5_init(&state);       
80        md5_append(&state, (const md5_byte_t *)key, strlen(key));
81        md5_append(&state, (const md5_byte_t *)&passdigest, 16);
82        md5_append(&state, (const md5_byte_t *)AIM_MD5_STRING, strlen(AIM_MD5_STRING));
83        md5_finish(&state, (md5_byte_t *)digest);
[5e53c4a]84
85        return 0;
86}
[cf02dd6]87#endif
[5e53c4a]88
89/*
[cf02dd6]90 * The FLAP version is sent by itself at the beginning of authorization
91 * connections.  The FLAP version is also sent before the cookie when connecting
92 * for other services (BOS, chatnav, chat, etc.).
[5e53c4a]93 */
94faim_export int aim_sendflapver(aim_session_t *sess, aim_conn_t *conn)
95{
96        aim_frame_t *fr;
97
98        if (!(fr = aim_tx_new(sess, conn, AIM_FRAMETYPE_FLAP, 0x01, 4)))
99                return -ENOMEM;
100
101        aimbs_put32(&fr->data, 0x00000001);
102
103        aim_tx_enqueue(sess, fr);
104
105        return 0;
106}
107
108/*
[cf02dd6]109 * This just pushes the passed cookie onto the passed connection, without
110 * the SNAC header or any of that.
[5e53c4a]111 *
[cf02dd6]112 * Very commonly used, as every connection except auth will require this to
113 * be the first thing you send.
[5e53c4a]114 *
115 */
[cf02dd6]116faim_export int aim_sendcookie(aim_session_t *sess, aim_conn_t *conn, const fu16_t length, const fu8_t *chipsahoy)
[5e53c4a]117{
118        aim_frame_t *fr;
119        aim_tlvlist_t *tl = NULL;
120
[cf02dd6]121        if (!(fr = aim_tx_new(sess, conn, AIM_FRAMETYPE_FLAP, 0x0001, 4+2+2+length)))
[5e53c4a]122                return -ENOMEM;
123
[cf02dd6]124        aimbs_put32(&fr->data, 0x00000001);
125        aim_tlvlist_add_raw(&tl, 0x0006, length, chipsahoy);
126        aim_tlvlist_write(&fr->data, &tl);
127        aim_tlvlist_free(&tl);
[5e53c4a]128
129        aim_tx_enqueue(sess, fr);
130
131        return 0;
132}
133
134/*
[862371b]135 * Part two of the ICQ hack.  Note the ignoring of the key.
[5e53c4a]136 */
[862371b]137static int goddamnicq2(aim_session_t *sess, aim_conn_t *conn, const char *sn, const char *password, struct client_info_s *ci)
[5e53c4a]138{
139        aim_frame_t *fr;
140        aim_tlvlist_t *tl = NULL;
[e374dee]141        int passwdlen;
142        fu8_t *password_encoded;
[5e53c4a]143
[e374dee]144        passwdlen = strlen(password);
145        if (!(password_encoded = (char *)malloc(passwdlen+1)))
[5e53c4a]146                return -ENOMEM;
[e374dee]147        if (passwdlen > MAXICQPASSLEN)
148                passwdlen = MAXICQPASSLEN;
[5e53c4a]149
150        if (!(fr = aim_tx_new(sess, conn, AIM_FRAMETYPE_FLAP, 0x01, 1152))) {
151                free(password_encoded);
152                return -ENOMEM;
153        }
154
155        aim_encode_password(password, password_encoded);
156
[862371b]157        aimbs_put32(&fr->data, 0x00000001); /* FLAP Version */
[cf02dd6]158        aim_tlvlist_add_raw(&tl, 0x0001, strlen(sn), sn);
159        aim_tlvlist_add_raw(&tl, 0x0002, passwdlen, password_encoded);
[862371b]160
161        if (ci->clientstring)
[cf02dd6]162                aim_tlvlist_add_raw(&tl, 0x0003, strlen(ci->clientstring), ci->clientstring);
163        aim_tlvlist_add_16(&tl, 0x0016, (fu16_t)ci->clientid);
164        aim_tlvlist_add_16(&tl, 0x0017, (fu16_t)ci->major);
165        aim_tlvlist_add_16(&tl, 0x0018, (fu16_t)ci->minor);
166        aim_tlvlist_add_16(&tl, 0x0019, (fu16_t)ci->point);
167        aim_tlvlist_add_16(&tl, 0x001a, (fu16_t)ci->build);
168        aim_tlvlist_add_32(&tl, 0x0014, (fu32_t)ci->distrib); /* distribution chan */
169        aim_tlvlist_add_raw(&tl, 0x000f, strlen(ci->lang), ci->lang);
170        aim_tlvlist_add_raw(&tl, 0x000e, strlen(ci->country), ci->country);
171
172        aim_tlvlist_write(&fr->data, &tl);
[5e53c4a]173
174        free(password_encoded);
[cf02dd6]175        aim_tlvlist_free(&tl);
[5e53c4a]176
177        aim_tx_enqueue(sess, fr);
178
179        return 0;
180}
181
182/*
[cf02dd6]183 * Subtype 0x0002
184 *
[5e53c4a]185 * This is the initial login request packet.
186 *
187 * NOTE!! If you want/need to make use of the aim_sendmemblock() function,
188 * then the client information you send here must exactly match the
189 * executable that you're pulling the data from.
190 *
191 * Java AIM 1.1.19:
192 *   clientstring = "AOL Instant Messenger (TM) version 1.1.19 for Java built 03/24/98, freeMem 215871 totalMem 1048567, i686, Linus, #2 SMP Sun Feb 11 03:41:17 UTC 2001 2.4.1-ac9, IBM Corporation, 1.1.8, 45.3, Tue Mar 27 12:09:17 PST 2001"
193 *   clientid = 0x0001
194 *   major  = 0x0001
195 *   minor  = 0x0001
196 *   point = (not sent)
197 *   build  = 0x0013
198 *   unknown= (not sent)
199 *   
200 * AIM for Linux 1.1.112:
201 *   clientstring = "AOL Instant Messenger (SM)"
202 *   clientid = 0x1d09
203 *   major  = 0x0001
204 *   minor  = 0x0001
205 *   point = 0x0001
206 *   build  = 0x0070
207 *   unknown= 0x0000008b
208 *   serverstore = 0x01
209 *
210 */
211faim_export int aim_send_login(aim_session_t *sess, aim_conn_t *conn, const char *sn, const char *password, struct client_info_s *ci, const char *key)
212{
213        aim_frame_t *fr;
214        aim_tlvlist_t *tl = NULL;
215        fu8_t digest[16];
216        aim_snacid_t snacid;
217
218        if (!ci || !sn || !password)
219                return -EINVAL;
220
[cf02dd6]221        /* If we're signing on an ICQ account then use the older, XOR login method */
222        if (isdigit(sn[0]))
[862371b]223                return goddamnicq2(sess, conn, sn, password, ci);
[5e53c4a]224
225        if (!(fr = aim_tx_new(sess, conn, AIM_FRAMETYPE_FLAP, 0x02, 1152)))
226                return -ENOMEM;
227
228        snacid = aim_cachesnac(sess, 0x0017, 0x0002, 0x0000, NULL, 0);
229        aim_putsnac(&fr->data, 0x0017, 0x0002, 0x0000, snacid);
230
[cf02dd6]231        aim_tlvlist_add_raw(&tl, 0x0001, strlen(sn), sn);
[5e53c4a]232
233        aim_encode_password_md5(password, key, digest);
[cf02dd6]234        aim_tlvlist_add_raw(&tl, 0x0025, 16, digest);
[5e53c4a]235
[cf02dd6]236#ifndef USE_OLD_MD5
237        aim_tlvlist_add_noval(&tl, 0x004c);
238#endif
[862371b]239
[5e53c4a]240        if (ci->clientstring)
[cf02dd6]241                aim_tlvlist_add_raw(&tl, 0x0003, strlen(ci->clientstring), ci->clientstring);
242        aim_tlvlist_add_16(&tl, 0x0016, (fu16_t)ci->clientid);
243        aim_tlvlist_add_16(&tl, 0x0017, (fu16_t)ci->major);
244        aim_tlvlist_add_16(&tl, 0x0018, (fu16_t)ci->minor);
245        aim_tlvlist_add_16(&tl, 0x0019, (fu16_t)ci->point);
246        aim_tlvlist_add_16(&tl, 0x001a, (fu16_t)ci->build);
247        aim_tlvlist_add_32(&tl, 0x0014, (fu32_t)ci->distrib);
248        aim_tlvlist_add_raw(&tl, 0x000f, strlen(ci->lang), ci->lang);
249        aim_tlvlist_add_raw(&tl, 0x000e, strlen(ci->country), ci->country);
[5e53c4a]250
[e374dee]251#ifndef NOSSI
[5e53c4a]252        /*
253         * If set, old-fashioned buddy lists will not work. You will need
254         * to use SSI.
255         */
[cf02dd6]256        aim_tlvlist_add_8(&tl, 0x004a, 0x01);
[e374dee]257#endif
[5e53c4a]258
[cf02dd6]259        aim_tlvlist_write(&fr->data, &tl);
[5e53c4a]260
[cf02dd6]261        aim_tlvlist_free(&tl);
[5e53c4a]262       
263        aim_tx_enqueue(sess, fr);
264
265        return 0;
266}
267
268/*
269 * This is sent back as a general response to the login command.
270 * It can be either an error or a success, depending on the
271 * precense of certain TLVs. 
272 *
273 * The client should check the value passed as errorcode. If
274 * its nonzero, there was an error.
275 */
276static int parse(aim_session_t *sess, aim_module_t *mod, aim_frame_t *rx, aim_modsnac_t *snac, aim_bstream_t *bs)
277{
278        aim_tlvlist_t *tlvlist;
279        aim_rxcallback_t userfunc;
[e374dee]280        struct aim_authresp_info *info;
[5e53c4a]281        int ret = 0;
282
[e374dee]283        info = (struct aim_authresp_info *)malloc(sizeof(struct aim_authresp_info));
284        memset(info, 0, sizeof(struct aim_authresp_info));
[5e53c4a]285
286        /*
287         * Read block of TLVs.  All further data is derived
288         * from what is parsed here.
289         */
[cf02dd6]290        tlvlist = aim_tlvlist_read(bs);
[5e53c4a]291
292        /*
293         * No matter what, we should have a screen name.
294         */
295        memset(sess->sn, 0, sizeof(sess->sn));
[cf02dd6]296        if (aim_tlv_gettlv(tlvlist, 0x0001, 1)) {
297                info->sn = aim_tlv_getstr(tlvlist, 0x0001, 1);
[e374dee]298                strncpy(sess->sn, info->sn, sizeof(sess->sn));
[5e53c4a]299        }
300
301        /*
302         * Check for an error code.  If so, we should also
303         * have an error url.
304         */
[cf02dd6]305        if (aim_tlv_gettlv(tlvlist, 0x0008, 1)) 
306                info->errorcode = aim_tlv_get16(tlvlist, 0x0008, 1);
307        if (aim_tlv_gettlv(tlvlist, 0x0004, 1))
308                info->errorurl = aim_tlv_getstr(tlvlist, 0x0004, 1);
[5e53c4a]309
310        /*
311         * BOS server address.
312         */
[cf02dd6]313        if (aim_tlv_gettlv(tlvlist, 0x0005, 1))
314                info->bosip = aim_tlv_getstr(tlvlist, 0x0005, 1);
[5e53c4a]315
316        /*
317         * Authorization cookie.
318         */
[cf02dd6]319        if (aim_tlv_gettlv(tlvlist, 0x0006, 1)) {
[5e53c4a]320                aim_tlv_t *tmptlv;
321
[cf02dd6]322                tmptlv = aim_tlv_gettlv(tlvlist, 0x0006, 1);
[5e53c4a]323
[e374dee]324                info->cookielen = tmptlv->length;
325                info->cookie = tmptlv->value;
[5e53c4a]326        }
327
328        /*
329         * The email address attached to this account
[e374dee]330         *   Not available for ICQ or @mac.com logins.
331         *   If you receive this TLV, then you are allowed to use
332         *   family 0x0018 to check the status of your email.
333         * XXX - Not really true!
[5e53c4a]334         */
[cf02dd6]335        if (aim_tlv_gettlv(tlvlist, 0x0011, 1))
336                info->email = aim_tlv_getstr(tlvlist, 0x0011, 1);
[5e53c4a]337
338        /*
339         * The registration status.  (Not real sure what it means.)
[e374dee]340         *   Not available for ICQ or @mac.com logins.
[5e53c4a]341         *
342         *   1 = No disclosure
343         *   2 = Limited disclosure
344         *   3 = Full disclosure
345         *
346         * This has to do with whether your email address is available
347         * to other users or not.  AFAIK, this feature is no longer used.
348         *
[e374dee]349         * Means you can use the admin family? (0x0007)
350         *
[5e53c4a]351         */
[cf02dd6]352        if (aim_tlv_gettlv(tlvlist, 0x0013, 1))
353                info->regstatus = aim_tlv_get16(tlvlist, 0x0013, 1);
354
355        if (aim_tlv_gettlv(tlvlist, 0x0040, 1))
356                info->latestbeta.build = aim_tlv_get32(tlvlist, 0x0040, 1);
357        if (aim_tlv_gettlv(tlvlist, 0x0041, 1))
358                info->latestbeta.url = aim_tlv_getstr(tlvlist, 0x0041, 1);
359        if (aim_tlv_gettlv(tlvlist, 0x0042, 1))
360                info->latestbeta.info = aim_tlv_getstr(tlvlist, 0x0042, 1);
361        if (aim_tlv_gettlv(tlvlist, 0x0043, 1))
362                info->latestbeta.name = aim_tlv_getstr(tlvlist, 0x0043, 1);
363        if (aim_tlv_gettlv(tlvlist, 0x0048, 1))
364                ; /* beta serial */
365
366        if (aim_tlv_gettlv(tlvlist, 0x0044, 1))
367                info->latestrelease.build = aim_tlv_get32(tlvlist, 0x0044, 1);
368        if (aim_tlv_gettlv(tlvlist, 0x0045, 1))
369                info->latestrelease.url = aim_tlv_getstr(tlvlist, 0x0045, 1);
370        if (aim_tlv_gettlv(tlvlist, 0x0046, 1))
371                info->latestrelease.info = aim_tlv_getstr(tlvlist, 0x0046, 1);
372        if (aim_tlv_gettlv(tlvlist, 0x0047, 1))
373                info->latestrelease.name = aim_tlv_getstr(tlvlist, 0x0047, 1);
374        if (aim_tlv_gettlv(tlvlist, 0x0049, 1))
375                ; /* lastest release serial */
[5e53c4a]376
[862371b]377        /*
378         * URL to change password.
379         */
[cf02dd6]380        if (aim_tlv_gettlv(tlvlist, 0x0054, 1))
381                info->chpassurl = aim_tlv_getstr(tlvlist, 0x0054, 1);
[e374dee]382
383        /*
384         * Unknown.  Seen on an @mac.com screen name with value of 0x003f
385         */
[cf02dd6]386        if (aim_tlv_gettlv(tlvlist, 0x0055, 1))
[e374dee]387                ;
388
389        sess->authinfo = info;
[5e53c4a]390
391        if ((userfunc = aim_callhandler(sess, rx->conn, snac ? snac->family : 0x0017, snac ? snac->subtype : 0x0003)))
[e374dee]392                ret = userfunc(sess, rx, info);
[5e53c4a]393
[cf02dd6]394        aim_tlvlist_free(&tlvlist);
[5e53c4a]395
396        return ret;
397}
398
399/*
[cf02dd6]400 * Subtype 0x0007 (kind of) - Send a fake type 0x0007 SNAC to the client
401 *
402 * This is a bit confusing.
403 *
404 * Normal SNAC login goes like this:
405 *   - connect
406 *   - server sends flap version
407 *   - client sends flap version
408 *   - client sends screen name (17/6)
409 *   - server sends hash key (17/7)
410 *   - client sends auth request (17/2 -- aim_send_login)
411 *   - server yells
412 *
413 * XOR login (for ICQ) goes like this:
414 *   - connect
415 *   - server sends flap version
416 *   - client sends auth request which contains flap version (aim_send_login)
417 *   - server yells
418 *
419 * For the client API, we make them implement the most complicated version,
420 * and for the simpler version, we fake it and make it look like the more
421 * complicated process.
422 *
423 * This is done by giving the client a faked key, just so we can convince
424 * them to call aim_send_login right away, which will detect the session
425 * flag that says this is XOR login and ignore the key, sending an ICQ
426 * login request instead of the normal SNAC one.
427 *
428 * As soon as AOL makes ICQ log in the same way as AIM, this is /gone/.
429 *
430 * XXX This may cause problems if the client relies on callbacks only
431 * being called from the context of aim_rxdispatch()...
432 *
433 */
434static int goddamnicq(aim_session_t *sess, aim_conn_t *conn, const char *sn)
435{
436        aim_frame_t fr;
437        aim_rxcallback_t userfunc;
438       
439        fr.conn = conn;
440       
441        if ((userfunc = aim_callhandler(sess, conn, 0x0017, 0x0007)))
442                userfunc(sess, &fr, "");
443
444        return 0;
445}
446
447/*
448 * Subtype 0x0006
449 *
450 * In AIM 3.5 protocol, the first stage of login is to request login from the
451 * Authorizer, passing it the screen name for verification.  If the name is
452 * invalid, a 0017/0003 is spit back, with the standard error contents.  If
453 * valid, a 0017/0007 comes back, which is the signal to send it the main
454 * login command (0017/0002).
455 *
456 */
457faim_export int aim_request_login(aim_session_t *sess, aim_conn_t *conn, const char *sn)
458{
459        aim_frame_t *fr;
460        aim_snacid_t snacid;
461        aim_tlvlist_t *tl = NULL;
462       
463        if (!sess || !conn || !sn)
464                return -EINVAL;
465
466        if (isdigit(sn[0]))
467                return goddamnicq(sess, conn, sn);
468
469        aim_sendflapver(sess, conn);
470
471        if (!(fr = aim_tx_new(sess, conn, AIM_FRAMETYPE_FLAP, 0x02, 10+2+2+strlen(sn) /*+8*/ )))
472                return -ENOMEM;
473
474        snacid = aim_cachesnac(sess, 0x0017, 0x0006, 0x0000, NULL, 0);
475        aim_putsnac(&fr->data, 0x0017, 0x0006, 0x0000, snacid);
476
477        aim_tlvlist_add_raw(&tl, 0x0001, strlen(sn), sn);
478/*      aim_tlvlist_add_noval(&tl, 0x004b);
479        aim_tlvlist_add_noval(&tl, 0x005a); */
480        aim_tlvlist_write(&fr->data, &tl);
481        aim_tlvlist_free(&tl);
482
483        aim_tx_enqueue(sess, fr);
484
485        return 0;
486}
487
488/*
489 * Subtype 0x0007
490 *
[5e53c4a]491 * Middle handler for 0017/0007 SNACs.  Contains the auth key prefixed
492 * by only its length in a two byte word.
493 *
494 * Calls the client, which should then use the value to call aim_send_login.
495 *
496 */
497static int keyparse(aim_session_t *sess, aim_module_t *mod, aim_frame_t *rx, aim_modsnac_t *snac, aim_bstream_t *bs)
498{
499        int keylen, ret = 1;
500        aim_rxcallback_t userfunc;
501        char *keystr;
502
503        keylen = aimbs_get16(bs);
504        keystr = aimbs_getstr(bs, keylen);
505
[e374dee]506        /* XXX - When GiantGrayPanda signed on AIM I got a thing asking me to register
507         * for the netscape network.  This SNAC had a type 0x0058 TLV with length 10. 
508         * Data is 0x0007 0004 3e19 ae1e 0006 0004 0000 0005 */
509
[5e53c4a]510        if ((userfunc = aim_callhandler(sess, rx->conn, snac->family, snac->subtype)))
511                ret = userfunc(sess, rx, keystr);
512
513        free(keystr); 
514
515        return ret;
516}
517
[e374dee]518static void auth_shutdown(aim_session_t *sess, aim_module_t *mod)
519{
520        if (sess->authinfo) {
521                free(sess->authinfo->sn);
522                free(sess->authinfo->bosip);
523                free(sess->authinfo->errorurl);
524                free(sess->authinfo->email);
525                free(sess->authinfo->chpassurl);
526                free(sess->authinfo->latestrelease.name);
527                free(sess->authinfo->latestrelease.url);
528                free(sess->authinfo->latestrelease.info);
529                free(sess->authinfo->latestbeta.name);
530                free(sess->authinfo->latestbeta.url);
531                free(sess->authinfo->latestbeta.info);
532                free(sess->authinfo);
533        }
534}
535
[5e53c4a]536static int snachandler(aim_session_t *sess, aim_module_t *mod, aim_frame_t *rx, aim_modsnac_t *snac, aim_bstream_t *bs)
537{
538
539        if (snac->subtype == 0x0003)
540                return parse(sess, mod, rx, snac, bs);
541        else if (snac->subtype == 0x0007)
542                return keyparse(sess, mod, rx, snac, bs);
543
544        return 0;
545}
546
547faim_internal int auth_modfirst(aim_session_t *sess, aim_module_t *mod)
548{
549
550        mod->family = 0x0017;
551        mod->version = 0x0000;
552        mod->flags = 0;
553        strncpy(mod->name, "auth", sizeof(mod->name));
554        mod->snachandler = snachandler;
[e374dee]555        mod->shutdown = auth_shutdown;
[5e53c4a]556
557        return 0;
558}
Note: See TracBrowser for help on using the repository browser.