Changeset 368:a1f26147ec61 in freeDiameter for extensions/app_radgw/rgwx_sip.c
- Timestamp:
- Jul 3, 2010, 12:40:00 AM (13 years ago)
- Branch:
- default
- Phase:
- public
- File:
-
- 1 edited
Legend:
- Unmodified
- Added
- Removed
-
extensions/app_radgw/rgwx_sip.c
r363 r368 49 49 #define CC_MULTIMEDIA_AUTH_REQUEST 286 /* MAR */ 50 50 #define CC_MULTIMEDIA_AUTH_ANSWER 286 /* MAA */ 51 #define ACV_ART_AUTHORIZE_AUTHENTICATE 3 /* AUTHORIZE_AUTHENTICATE */52 #define ACV_OAP_RADIUS 1 /* RADIUS */53 51 #define ACV_ASS_STATE_MAINTAINED 0 /* STATE_MAINTAINED */ 54 52 #define ACV_ASS_NO_STATE_MAINTAINED 1 /* NO_STATE_MAINTAINED */ … … 140 138 struct session_handler * sess_hdl; /* We store RADIUS request authenticator information in the session */ 141 139 char * confstr; 142 // Global variable which points to chained list of nonce140 //Chained list of nonce 143 141 struct fd_list listnonce; 144 142 //This will be used to lock access to chained list … … 151 149 struct fd_list chain; 152 150 char * sid; 151 size_t sidlen; 153 152 char * nonce; 153 size_t noncelen; 154 154 155 155 }; 156 156 157 158 159 160 161 162 int nonce_add_element(char * nonce, char * sid, struct rgwp_config *state) 157 static int nonce_add_element(char * nonce, size_t noncelen,char * sid, size_t sidlen, struct rgwp_config * state) 163 158 { 159 CHECK_PARAMS(nonce && state && sid && sidlen && noncelen); 160 164 161 noncechain *newelt; 165 162 CHECK_MALLOC(newelt=malloc(sizeof(noncechain))); 166 int lenghtsid=strlen(sid);167 168 CHECK_MALLOC(newelt->nonce=malloc(33));169 memcpy(newelt->nonce,nonce,32);170 newelt->nonce[32]='\0';171 CHECK_MALLOC(newelt->sid=malloc( lenghtsid+1));172 strncpy(newelt->sid,sid,lenghtsid);173 newelt->sid [lenghtsid]='\0';174 175 FD_LIST_INITIALIZER(&newelt->chain);163 164 CHECK_MALLOC(newelt->nonce=malloc(noncelen)); 165 memcpy(newelt->nonce,nonce,noncelen); 166 newelt->noncelen=noncelen; 167 168 CHECK_MALLOC(newelt->sid=malloc(sidlen)); 169 memcpy(newelt->sid,sid,sidlen); 170 newelt->sidlen=sidlen; 171 172 fd_list_init(&newelt->chain,NULL); 176 173 177 174 CHECK_POSIX(pthread_mutex_lock(&state->nonce_mutex)); 178 175 fd_list_insert_before(&state->listnonce,&newelt->chain); 179 176 CHECK_POSIX(pthread_mutex_unlock(&state->nonce_mutex)); 177 178 return 0; 180 179 } 181 180 182 void nonce_del_element(char * nonce, struct rgwp_config *state)181 static void nonce_del_element(char * nonce, struct rgwp_config *state) 183 182 { 184 if(!FD_IS_LIST_EMPTY(&state->listnonce)) 185 { 186 /* 187 noncechain *temp=listnonce, *tempbefore=NULL; 188 189 if(listnonce->next==NULL && strcmp(listnonce->nonce,nonce)==0) 183 struct fd_list * li; 184 185 CHECK_PARAMS_DO(nonce && state, return); 186 187 for(li=state->listnonce.next;li!=&state->listnonce;li=li->next) 188 { 189 noncechain *temp=(noncechain *)li; 190 191 if(strcmp(temp->nonce,nonce)==0) 190 192 { 191 f ree(listnonce->nonce);192 free( listnonce->sid);193 free( listnonce);194 listnonce=NULL;195 return;193 fd_list_unlink (li); 194 free(temp->sid); 195 free(temp->nonce); 196 free(temp); 197 break; 196 198 } 197 while(temp->next != NULL) 199 } 200 } 201 202 //Retrieve sid from nonce 203 static char * nonce_get_sid(char * nonce, size_t noncelen, size_t * sidlen, struct rgwp_config *state) 204 { 205 struct fd_list * li; 206 char *sid=NULL; 207 208 CHECK_PARAMS_DO(nonce && state && noncelen && sidlen, return); 209 *sidlen=0; 210 211 //**Start mutex 212 CHECK_POSIX_DO(pthread_mutex_lock(&state->nonce_mutex),); 213 for(li=state->listnonce.next;li!=&state->listnonce;li=li->next) 214 { 215 noncechain *temp=(noncechain *)li; 216 217 if(temp->noncelen==noncelen && strncmp(temp->nonce,nonce, noncelen)==0) 198 218 { 199 if(strcmp(temp->nonce,nonce)==0) 200 { 201 if(tempbefore==NULL) 202 { 203 listnonce=temp->next; 204 free(temp->nonce); 205 free(temp->sid); 206 free(temp); 207 return; 208 } 209 tempbefore->next=temp->next; 210 free(temp->nonce); 211 free(temp->sid); 212 free(temp); 213 break; 214 } 215 tempbefore=temp; 216 temp = temp->next; 217 }*/ 218 } 219 219 fd_list_unlink (li); 220 sid=temp->sid; 221 *sidlen=temp->sidlen; 222 free(temp->nonce); 223 free(temp); 224 break; 225 } 226 227 } 228 CHECK_POSIX_DO(pthread_mutex_unlock(&state->nonce_mutex),); 229 //***Stop mutex 230 return sid; 220 231 } 221 //Retrieve sid from nonce 222 char * nonce_check_element(char * nonce)232 233 static void nonce_deletelistnonce(struct rgwp_config *state) 223 234 { 224 /* 225 if(listnonce==NULL) 226 { 227 //Not found 228 return NULL; 229 } 230 else 231 { 232 noncechain* temp=listnonce; 233 234 if(strcmp(temp->nonce,nonce)==0) 235 return temp->sid; 236 237 while(temp->next != NULL) 238 { 239 240 if(strcmp(temp->nonce,nonce)==0) 241 { 242 TRACE_DEBUG(FULL,"We found the nonce!"); 243 return temp->sid; 244 } 245 else 246 temp = temp->next; 247 } 248 249 250 } 251 return NULL; 252 */ 253 } 254 255 void nonce_deletelistnonce() 256 { 257 /* 258 if(listnonce !=NULL) 259 { 260 while(listnonce->next != NULL) 261 { 262 noncechain* temp=listnonce->next; 263 264 free(listnonce->nonce); 265 free(listnonce->sid); 266 free(listnonce); 267 268 listnonce=temp; 269 } 270 free(listnonce->nonce); 271 free(listnonce->sid); 272 free(listnonce); 273 listnonce=NULL; 274 } 275 */ 235 //**Start mutex 236 CHECK_POSIX_DO(pthread_mutex_lock(&state->nonce_mutex),); 237 while(!(FD_IS_LIST_EMPTY(&state->listnonce)) ) 238 { 239 noncechain *temp=(noncechain *)state->listnonce.next; 240 241 fd_list_unlink (&temp->chain); 242 free(temp->sid); 243 free(temp->nonce); 244 free(temp); 245 246 } 247 CHECK_POSIX_DO(pthread_mutex_unlock(&state->nonce_mutex),); 248 //***Stop mutex 276 249 } 277 250 … … 330 303 331 304 //chained list 332 FD_LIST_INITIALIZER(&new->listnonce);305 fd_list_init(&new->listnonce,NULL); 333 306 CHECK_POSIX(pthread_mutex_init(&new->nonce_mutex,NULL)); 334 307 … … 345 318 CHECK_FCT_DO( fd_sess_handler_destroy( &state->sess_hdl ), ); 346 319 347 nonce_deletelistnonce( &state->listnonce);320 nonce_deletelistnonce(state); 348 321 CHECK_POSIX_DO(pthread_mutex_destroy(&state->nonce_mutex), /*continue*/); 349 322 … … 357 330 { 358 331 int idx; 359 int got_username = 0;360 332 int got_AOR = 0; 361 333 int got_Dusername = 0; … … 369 341 int got_Dresponse = 0; 370 342 int got_Dalgorithm = 0; 371 343 char * sid = NULL; 344 char * un=NULL; 345 size_t un_len; 372 346 uint32_t status_type; 373 347 size_t nattr_used = 0; … … 377 351 TRACE_ENTRY("%p %p %p %p %p %p", cs, session, rad_req, rad_ans, diam_fw, cli); 378 352 379 CHECK_PARAMS(rad_req && (rad_req->hdr->code == RADIUS_CODE_ACCESS_REQUEST) && rad_ans && diam_fw && *diam_fw );353 CHECK_PARAMS(rad_req && (rad_req->hdr->code == RADIUS_CODE_ACCESS_REQUEST) && rad_ans && diam_fw && *diam_fw && session); 380 354 381 355 //We check that session is not already filled … … 398 372 399 373 case RADIUS_ATTR_USER_NAME: 400 got_username = 1; 374 if (attr->length>sizeof(struct radius_attr_hdr)) 375 { 376 TRACE_DEBUG(ANNOYING, "Found a User-Name attribute: '%.*s'", attr->length- sizeof(struct radius_attr_hdr), (char *)(attr+1)); 377 un = (char *)(attr + 1); 378 un_len =attr->length - sizeof(struct radius_attr_hdr); 379 } 401 380 break; 402 381 case RADIUS_ATTR_DIGEST_USERNAME: … … 420 399 case RADIUS_ATTR_DIGEST_NONCE: 421 400 got_Dnonce = 1; 401 402 size_t sidlen; 403 404 sid=nonce_get_sid((char *)(attr+1),attr->length-2,&sidlen,cs); 405 if(!sid) 406 { 407 TRACE_DEBUG(INFO,"We haven't found the session.'"); 408 return EINVAL; 409 } 410 CHECK_FCT(fd_sess_fromsid (sid, sidlen, session, NULL)); 411 free(sid); 412 413 422 414 break; 423 415 case RADIUS_ATTR_DIGEST_CNONCE: … … 435 427 } 436 428 } 437 if(! got_username)429 if(!un) 438 430 { 439 431 TRACE_DEBUG(INFO,"No Username in request"); 440 return 1; 441 } 442 if(!got_Dnonce) 443 { 444 /* Add the Session-Id AVP as first AVP */ 445 CHECK_FCT( fd_msg_avp_new ( cs->dict.Session_Id, 0, &avp ) ); 446 447 char *sid=NULL; 448 fd_sess_getsid (session, &sid ); 449 memset(&value, 0, sizeof(value)); 450 value.os.data = (unsigned char *)sid; 451 value.os.len = strlen(sid); 452 CHECK_FCT( fd_msg_avp_setvalue ( avp, &value ) ); 453 CHECK_FCT( fd_msg_avp_add ( *diam_fw, MSG_BRW_FIRST_CHILD, avp) ); 454 } 432 return EINVAL; 433 } 434 435 /* Create the session if it is not already done */ 436 if (!*session) { 437 438 char * fqdn; 439 char * realm; 440 441 442 443 444 /* Get information on the RADIUS client */ 445 CHECK_FCT( rgw_clients_get_origin(cli, &fqdn, &realm) ); 446 447 int len; 448 /* Create a new Session-Id. The format is: {fqdn;hi32;lo32;username;diamid} */ 449 CHECK_MALLOC( sid = malloc(un_len + 1 /* ';' */ + fd_g_config->cnf_diamid_len + 1 /* '\0' */) ); 450 len = sprintf(sid, "%.*s;%s", un_len, un, fd_g_config->cnf_diamid); 451 CHECK_FCT( fd_sess_new(session, fqdn, sid, len) ); 452 free(sid); 453 } 454 455 /* Add the Destination-Realm AVP */ 456 CHECK_FCT( fd_msg_avp_new ( cs->dict.Destination_Realm, 0, &avp ) ); 457 458 int i = 0; 459 if (un) { 460 /* Is there an '@' in the user name? We don't care for decorated NAI here */ 461 for (i = un_len - 2; i > 0; i--) { 462 if (un[i] == '@') { 463 i++; 464 break; 465 } 466 } 467 } 468 if (i == 0) { 469 /* Not found in the User-Name => we use the local domain of this gateway */ 470 value.os.data = fd_g_config->cnf_diamrlm; 471 value.os.len = fd_g_config->cnf_diamrlm_len; 472 } else { 473 value.os.data = un + i; 474 value.os.len = un_len - i; 475 } 476 477 CHECK_FCT( fd_msg_avp_setvalue ( avp, &value ) ); 478 CHECK_FCT( fd_msg_avp_add ( *diam_fw, MSG_BRW_FIRST_CHILD, avp) ); 479 480 /* Now, add the Session-Id AVP at beginning of Diameter message */ 481 CHECK_FCT( fd_sess_getsid(*session, &sid) ); 482 483 TRACE_DEBUG(FULL, "[sip.rgwx] Translating new message for session '%s'...", sid); 484 485 /* Add the Session-Id AVP as first AVP */ 486 CHECK_FCT( fd_msg_avp_new ( cs->dict.Session_Id, 0, &avp ) ); 487 value.os.data = (unsigned char *)sid; 488 value.os.len = strlen(sid); 489 CHECK_FCT( fd_msg_avp_setvalue ( avp, &value ) ); 490 CHECK_FCT( fd_msg_avp_add ( *diam_fw, MSG_BRW_FIRST_CHILD, avp) ); 491 455 492 /* 456 493 If the RADIUS Access-Request message does not … … 641 678 case RADIUS_ATTR_DIGEST_NONCE: 642 679 CONV2DIAM_STR_AUTH( Digest_Nonce ); 643 644 645 int new=0;646 int sidlen=0;647 struct session * temp;648 char *nonce=malloc(attr->length-1);649 char *sid=malloc(sidlen+1);650 651 strncpy(nonce,(char *)(attr+1), attr->length-2);652 nonce[attr->length-2]='\0';653 654 //**Start mutex655 pthread_mutex_lock(&state->nonce_mutex);656 sidlen=strlen(nonce_check_element(nonce));657 strcpy(sid,nonce_check_element(nonce));658 sid[sidlen+1]='\0';659 nonce_del_element(nonce);660 free(nonce); //TODO: free nonce inside delete661 pthread_mutex_unlock(&state->nonce_mutex);662 //**Stop mutex663 664 CHECK_FCT(fd_sess_fromsid ( (char *)sid, (size_t)sidlen, &temp, &new));665 //free(sid);666 667 if(new==0)668 {669 session=temp;670 /* Add the Session-Id AVP as first AVP */671 CHECK_FCT( fd_msg_avp_new ( cs->dict.Session_Id, 0, &avp ) );672 //memset(&value, 0, sizeof(value));673 value.os.data = (unsigned char *)sid;674 value.os.len = sidlen;675 CHECK_FCT( fd_msg_avp_setvalue ( avp, &value ) );676 CHECK_FCT( fd_msg_avp_add ( *diam_fw, MSG_BRW_FIRST_CHILD, avp) );677 678 }679 else680 {681 TRACE_DEBUG(INFO,"Can't find previously established session, message droped!");682 return 1;683 }684 //free(sid);685 free(nonce);686 //fd_sess_dump(FULL,session);687 688 689 690 680 break; 691 681 case RADIUS_ATTR_DIGEST_NONCE_COUNT: … … 738 728 739 729 740 if ( session) {730 if (*session) { 741 731 unsigned char * req_sip; 742 732 CHECK_MALLOC(req_sip = malloc(16)); 743 733 memcpy(req_sip, &rad_req->hdr->authenticator[0], 16); 744 734 745 CHECK_FCT( fd_sess_state_store( cs->sess_hdl, session, &req_sip ) );735 CHECK_FCT( fd_sess_state_store( cs->sess_hdl, *session, &req_sip ) ); 746 736 } 747 737 … … 755 745 struct msg_hdr * hdr; 756 746 struct avp *avp, *next, *asid; 757 struct avp_hdr *ahdr, *sid, *oh; 758 char buf[254]; /* to store some attributes values (with final '\0') */ 759 int ta_set = 0; 760 int no_str = 0; /* indicate if an STR is required for this server */ 761 uint8_t tuntag = 0; 762 unsigned char * req_sip = NULL; 747 struct avp_hdr *ahdr, *sid; 748 //char buf[254]; /* to store some attributes values (with final '\0') */ 749 //unsigned char * req_sip = NULL; 763 750 int in_success=0; 764 751 … … 801 788 CHECK_FCT( fd_msg_search_avp (*diam_ans, cs->dict.Session_Id, &asid) ); 802 789 CHECK_FCT( fd_msg_avp_hdr ( asid, &sid ) ); 803 CHECK_FCT( fd_msg_search_avp (*diam_ans, cs->dict.Origin_Host, &asid) );804 CHECK_FCT( fd_msg_avp_hdr ( asid, &oh ) );805 790 806 791 /* Check the Diameter error code */ … … 812 797 case ER_DIAMETER_SUCCESS_AUTH_SENT_SERVER_NOT_STORED: 813 798 (*rad_fw)->hdr->code = RADIUS_CODE_ACCESS_CHALLENGE; 814 *statefull=1; 815 struct timespec nowts; 816 CHECK_SYS(clock_gettime(CLOCK_REALTIME, &nowts)); 817 nowts.tv_sec+=600; 818 CHECK_FCT(fd_sess_settimeout(session, &nowts )); 799 //struct timespec nowts; 800 //CHECK_SYS(clock_gettime(CLOCK_REALTIME, &nowts)); 801 //nowts.tv_sec+=600; 802 //CHECK_FCT(fd_sess_settimeout(session, &nowts )); 819 803 break; 820 804 case ER_DIAMETER_SUCCESS_SERVER_NAME_NOT_STORED: … … 826 810 default: 827 811 (*rad_fw)->hdr->code = RADIUS_CODE_ACCESS_REJECT; 828 fd_log_debug("[ authSIP.rgwx] Received Diameter answer with error code '%d' from server '%.*s', session %.*s, translating into Access-Reject\n",812 fd_log_debug("[sip.rgwx] Received Diameter answer with error code '%d', session %.*s, translating into Access-Reject\n", 829 813 ahdr->avp_value->u32, 830 oh->avp_value->os.len, oh->avp_value->os.data,831 814 sid->avp_value->os.len, sid->avp_value->os.data); 832 815 return 0; … … 834 817 /* Remove this Result-Code avp */ 835 818 CHECK_FCT( fd_msg_free( avp ) ); 836 837 /* Creation of the State or Class attribute with session information */838 CHECK_FCT( fd_msg_search_avp (*diam_ans, cs->dict.Origin_Realm, &avp) );839 CHECK_FCT( fd_msg_avp_hdr ( avp, &ahdr ) );840 841 842 /* Now, save the session-id and eventually server info in a STATE or CLASS attribute */843 if ((*rad_fw)->hdr->code == RADIUS_CODE_ACCESS_CHALLENGE) {844 if (sizeof(buf) < snprintf(buf, sizeof(buf), "Diameter/%.*s/%.*s/%.*s",845 oh->avp_value->os.len, oh->avp_value->os.data,846 ahdr->avp_value->os.len, ahdr->avp_value->os.data,847 sid->avp_value->os.len, sid->avp_value->os.data)) {848 TRACE_DEBUG(INFO, "Data truncated in State attribute: %s", buf);849 }850 CONV2RAD_STR(RADIUS_ATTR_STATE, buf, strlen(buf), 0);851 852 }853 854 if ((*rad_fw)->hdr->code == RADIUS_CODE_ACCESS_ACCEPT) {855 /* Add the Session-Id */856 if (sizeof(buf) < snprintf(buf, sizeof(buf), "Diameter/%.*s",857 sid->avp_value->os.len, sid->avp_value->os.data)) {858 TRACE_DEBUG(INFO, "Data truncated in Class attribute: %s", buf);859 }860 CONV2RAD_STR(RADIUS_ATTR_CLASS, buf, strlen(buf), 0);861 }862 863 /* Unlink the Origin-Realm now; the others are unlinked at the end of this function */864 CHECK_FCT( fd_msg_free( avp ) );865 866 867 819 868 820 /* Now loop in the list of AVPs and convert those that we know how */ … … 879 831 switch (ahdr->avp_code) { 880 832 881 case DIAM_ATTR_AUTH_SESSION_STATE: 882 if ((!ta_set) && (ahdr->avp_value->u32 == ACV_ASS_STATE_MAINTAINED)) { 883 CONV2RAD_32B( RADIUS_ATTR_TERMINATION_ACTION, RADIUS_TERMINATION_ACTION_RADIUS_REQUEST ); 884 } 885 886 if (ahdr->avp_value->u32 == ACV_ASS_NO_STATE_MAINTAINED) { 887 no_str = 1; 888 } 889 break; 833 890 834 case DIAM_ATTR_DIGEST_NONCE: 891 835 CONV2RAD_STR(DIAM_ATTR_DIGEST_NONCE, ahdr->avp_value->os.data, ahdr->avp_value->os.len, 0); … … 893 837 if (session) { 894 838 char *sid=NULL; 839 size_t sidlen; 840 fd_sess_getsid (session, &sid ); 841 sidlen=strlen(sid); 895 842 896 fd_sess_getsid (session, &sid ); 897 898 //***Start mutex 899 CHECK_POSIX(pthread_mutex_lock(&state->nonce_mutex)); 900 nonce_add_element(ahdr->avp_value->os.data, sid, state); 901 CHECK_POSIX(pthread_mutex_unlock(&state->nonce_mutex)); 902 //***Stop mutex 843 nonce_add_element(ahdr->avp_value->os.data,ahdr->avp_value->os.len, sid,sidlen, cs); 903 844 } 904 845 break; … … 926 867 } 927 868 } 928 929 930 931 932 CHECK_FCT( fd_sess_state_retrieve( cs->sess_hdl, session, &req_sip ) );933 }934 } 935 936 req_sip=NULL;869 } 870 871 if (session) 872 { 873 //TODO: authenticator & message-authenticator 874 CHECK_FCT( fd_sess_state_retrieve( cs->sess_hdl, session, &req_sip ) ); 875 } 876 free(req_sip); 877 937 878 938 879 return 0; … … 946 887 .rgwp_rad_req = sip_rad_req, 947 888 .rgwp_diam_ans = sip_diam_ans 948 949 889 }; 950 /*} 951 /* Add FAKE Digest_Realm AVP 952 { 953 //We give a fake realm because it will be provided in the second access request. 954 CHECK_FCT( fd_msg_avp_new ( cs->dict.Digest_Realm, 0, &avp ) ); 955 956 u8 *realm="example.com"; 957 958 value.os.data=(unsigned char *)realm; 959 value.os.len=strlen(realm); 960 CHECK_FCT( fd_msg_avp_setvalue ( avp, &value ) ); 961 CHECK_FCT( fd_msg_avp_add ( auth, MSG_BRW_LAST_CHILD, avp) ); 962 963 } 964 else 965 { 966 TRACE_DEBUG(FULL,"\nAnswer to challenge!\n"); 967 //We need a client nonce, count nonce, digest realm, username and response to handle authentication 968 if (got_Dnonce_count && got_Dcnonce && got_Dresponse && got_Drealm && got_Dusername) 969 { 970 /* Add SIP_Authorization AVP 971 { 972 CHECK_FCT( fd_msg_avp_new ( cs->dict.SIP_Authorization, 0, &auth ) ); 973 CHECK_FCT( fd_msg_avp_add ( auth_data, MSG_BRW_LAST_CHILD, auth) ); 974 } 975 for (idx = 0; idx < rad_req->attr_used; idx++) 976 { 977 struct radius_attr_hdr * attr = (struct radius_attr_hdr *)(rad_req->buf + rad_req->attr_pos[idx]); 978 char * temp; 979 980 switch (attr->type) { 981 982 983 default: 984 985 if(!got_Dalgorithm) 986 { 987 //[Note 3] If Digest-Algorithm is missing, 'MD5' is assumed. 988 989 CHECK_PARAMS( attr->length >= 2 ); 990 CHECK_FCT( fd_msg_avp_new ( cs->dict.Digest_Algorithm, 0, &avp ) ); 991 value.os.len = attr->length - 2; 992 value.os.data = (unsigned char *)(attr + 1); 993 CHECK_FCT( fd_msg_avp_setvalue ( avp, &value ) ); 994 CHECK_FCT( fd_msg_avp_add ( auth, MSG_BRW_LAST_CHILD, avp) ); 995 } 996 997 } 998 999 } 1000 CHECK_FCT( fd_msg_avp_add ( *diam_fw, MSG_BRW_LAST_CHILD, auth_data) ); 1001 1002 } 1003 else 1004 { 1005 TRACE_DEBUG(INFO,"Missing Digest attributes in request, we drop it..."); 1006 return 1; 1007 } 1008 }*/ 1009 890
Note: See TracChangeset
for help on using the changeset viewer.