diff -u -r src-old/acct.c src-orlogic/acct.c --- src-old/acct.c Tue Jun 12 04:31:23 2001 +++ src-orlogic/acct.c Sun May 12 02:09:39 2002 @@ -316,7 +316,8 @@ /* Write each attribute/value to the log file */ pair = authreq->request; - fprint_attr_list_csv(outfd, pair, 0); + /* Verbose format changed by vi@maks.net */ + fprint_attr_list(outfd, pair); fputs("\n", outfd); fclose(outfd); } diff -u -r src-old/attrprint.c src-orlogic/attrprint.c --- src-old/attrprint.c Tue Sep 5 20:16:10 2000 +++ src-orlogic/attrprint.c Fri Nov 9 10:54:37 2001 @@ -87,8 +87,8 @@ int i; #endif + fprintf (fd, "group %s ", pair->group_name); switch(pair->type) { - case PW_TYPE_STRING: fprintf(fd, "%s = \"", pair->name); ptr = (u_char *)pair->strvalue; diff -u -r src-old/auth.c src-orlogic/auth.c --- src-old/auth.c Wed Jun 6 20:21:33 2001 +++ src-orlogic/auth.c Tue Feb 12 12:54:24 2002 @@ -422,8 +422,11 @@ */ snprintf(dbpass, 128, " Password should be '%s'", password_pair->strvalue); if (password_pair == NULL || - strcmp(password_pair->strvalue, string)!=0) + strcmp(password_pair->strvalue, string)!=0) + { + DEBUG2("Passwords do not match (%s vs %s)", password_pair->strvalue, string); result = -1; + } break; } @@ -627,12 +630,23 @@ string[AUTH_PASS_LEN] = '\0'; strcpy(userpass, string); } + + DEBUG2("Getting groups for realm %s", authreq->realm); + sql_getvpdata(socket, sql->config->sql_groupcheck_table, &authreq->check_pairs, authreq->realm, PW_VP_REALMDATA); sql_getvpdata(socket, sql->config->sql_groupreply_table, &authreq->reply_pairs, authreq->realm, PW_VP_REALMDATA); if (authreq->server_code == PW_AUTHENTICATION_ACK) { result = 0; + if (paircmp_group(authreq->request, &authreq->check_pairs, &authreq->reply_pairs) != 0) { + log(L_AUTH, "Realm check list does not match request list [%s] (%s)", namepair->strvalue, auth_name(authreq, 1)); + rad_send_reply(PW_AUTHENTICATION_REJECT, authreq, NULL, NULL, activefd); + authfree(authreq); + return -1; + } + + if((result = check_expiration(authreq->check_pairs, umsg, &user_msg)) < 0) { rad_send_reply(PW_AUTHENTICATION_REJECT, authreq, authreq->reply_pairs, user_msg, activefd); log(L_AUTH, "Realm Expired: [%s] (%s)", authreq->realm, auth_name(authreq, 1)); @@ -958,14 +972,15 @@ return -1; } - if (paircmp(authreq->request, authreq->check_pairs) != 0) { + if (paircmp_group(authreq->request, &authreq->check_pairs, &authreq->reply_pairs) != 0) { log(L_AUTH, "Check list does not match request list [%s] (%s)", namepair->strvalue, auth_name(authreq, 1)); rad_send_reply(PW_AUTHENTICATION_REJECT, authreq, NULL, NULL, activefd); authfree(authreq); return -1; } - - + + if (debug_flag) fprint_attr_list(stdout,authreq->reply_pairs); + /* * Validate the user */ Only in src-orlogic: conf.h.orig diff -u -r src-old/files.c src-orlogic/files.c --- src-old/files.c Tue Jun 12 04:31:23 2001 +++ src-orlogic/files.c Tue Feb 12 13:04:04 2002 @@ -339,6 +339,7 @@ return retval; } + /* * Compare prefix/suffix. */ @@ -391,8 +392,6 @@ return ret; } - - /* * Compare two pair lists except for the password information. * Return 0 on match. @@ -550,6 +549,318 @@ } +/* Same as paircmp, but makes OR logic for groups having Fall-Through check item. * + * OR logic for group membership: if a group has "Fall-Through = Yes" + * check item, then if this group check items are not satisfied group's + * reply items will be removed and authentication will not fail. + * OR logic for group check items:: if group's name contains "__OR__", + * then all group's check items will be ORed with each other. That means, + * if one of the check items succeeds, group membership will suceed as + * well, and group reply items will be added to request reply list. + * Useful for phone number blacklists. + * AND logic for groups:If a group has Auth-Type: Accept check item and + * group check items are satisfied, then all Auth-Type: Reject's will be + * removed from the check item lists. This allows for AND logic, where it + * is required for at least one group membership to succeed, or + * authentication will fail. Please note that groups are scanned in + * ALPHABETICAL order, so a good idea is to add a special deny group, + * like "_ACCESS_CHECK", which has Auth-Type: Reject. + * NOT logic for groups:If a group has Auth-Type: Reject check item and + * group check items are satisfied, then authentication request will fail + * immediately. + * Please note that various types of logic can be combined (where it + * makes sense). Like, you can combine OR logic for group membership with + * OR logic for group check items, OR logic for group check items with + * AND logic for groups and so on. + * + * Patch made by Vladimir Ivaschenko , http://www.hazard.maks.net + */ + +int paircmp_group(VALUE_PAIR *request, VALUE_PAIR **check, VALUE_PAIR **reply_pairs) { + VALUE_PAIR *check_item = *check; + VALUE_PAIR *auth_item; + VALUE_PAIR *reject_check_item_is_next; + char *cur_group = (*check)->group_name; + int result = 0, old_result = -1; + char username[AUTH_STRING_LEN]; + int compare; + int or_logic = 0; + int reject_logic = 0; + int accept_logic = 0; + int auth_type = 0; + VALUE_PAIR *i, *next, *last = NULL; + + reject_check_item_is_next = NULL; + + while (1) { + DEBUG2("cur_group: %s check_item_group: %s result: %i or_logic: %u", + cur_group, (check_item != NULL ? check_item->group_name : "__LAST__"), + result, or_logic); + if (strncmp((check_item != NULL ? check_item->group_name : "__LAST__"), + cur_group, sizeof(check_item != NULL ? check_item->group_name : "__LAST__")) != 0) + { + /* OR check item logic checks */ + if (strstr(cur_group, "__OR__")) { + result = old_result; + } + /* AUTH_TYPE_REJECT was found in the group, and group + * membership conditions were satisfied: REJECT then. */ + if (result == 0 && reject_logic == 1) { + DEBUG2("AUTH-TYPE: REJECT was found in group checklist, rejecting request"); + result = -1; break; + } + else if (reject_logic == 1) { + /* Remove AUTH_TYPE_REJECT if membership didn't succeed */ + DEBUG2("%s membership didn't succeed, removing Auth-Type: Reject", cur_group); + if (reject_check_item_is_next != NULL) + { + i = reject_check_item_is_next->next; + reject_check_item_is_next = reject_check_item_is_next->next->next; + free(i); + } else + DEBUG2("reject_check_item_is_next == NULL, should not ever happen!"); + } + /* AUTH_TYPE_ACCEPT was found in the group, and group + * membership conditions were satisfied: remove ALL + * rejects from check items */ + if (result == 0 && accept_logic == 1) + { + DEBUG2("AUTH-TYPE: ACCEPT was found in group checklist, removing all possible AUTH-TYPE: REJECTs"); + for(i = *check; i; i = next) { + next = i->next; + if (strcmp(i->group_name,(*check)->group_name) != 0 && i->attribute == PW_AUTHTYPE && i->lvalue == PW_AUTHTYPE_REJECT) { + /* check if we already started parsing this item */ + if (check_item == i) check_item=next; + last->next = next; + free(i); + } else + last = i; + } + } + /* OR group selection logic checks */ + if (result != 0 && or_logic == 1) + { + /* Delete failed group's reply items */ + DEBUG2("REMOVING %s's reply items", cur_group); + result = 0; + pairdelete_group(reply_pairs, cur_group); + fprint_attr_list(stdout,*reply_pairs); + DEBUG2("REMOVING %s's check items", cur_group); + for(i = *check; i; i = next) { + next = i->next; + if (strcmp(i->group_name,cur_group) == 0) { + /* check if we already started parsing this item */ + if (check_item == i) check_item=next; + last->next = next; + free(i); + } else + last = i; + } + fprint_attr_list(stdout,*check); + + } + else if (result != 0) { break; } /* Fail if not an OR group */ + or_logic = 0; /* result OR logic indicator */ + if (check_item) cur_group = check_item->group_name; /* change current group name */ + old_result = -1; + reject_logic = 0; + } + + if (check_item == NULL) break; + + if (check_item->next != NULL && check_item->next->lvalue == PW_AUTHTYPE_REJECT) { + reject_check_item_is_next = check_item; + } + + switch (check_item->attribute) { + /* + * Attributes we skip during comparison. + * These are "server" check items. + */ + case PW_AUTHTYPE: + { + auth_type = check_item->lvalue; + /* reject_logic and accept_logic work only for + * groups */ + if (auth_type == PW_AUTHTYPE_REJECT && strcmp((*check)->group_name,check_item->group_name) != 0) { + DEBUG2("%s has REJECT logic", cur_group); + reject_logic = 1; + } + else if (auth_type == PW_AUTHTYPE_ACCEPT && strcmp((*check)->group_name,check_item->group_name) != 0) { + DEBUG2("%s has ACCEPT logic", cur_group); + accept_logic = 1; + } + check_item = check_item->next; + continue; + } + case PW_FALL_THROUGH: + { + DEBUG2("%s is FALL THROUGH", cur_group); + or_logic = 1; + check_item = check_item->next; + continue; + } + case PW_EXPIRATION: + case PW_LOGIN_TIME: + case PW_PASSWORD: + case PW_CRYPT_PASSWORD: +#ifdef PAM /* cjd 19980706 */ + case PAM_AUTH_ATTR: +#endif + case PW_SIMULTANEOUS_USE: + case PW_MAX_HOURS: + case PW_MONTHLY_TIME_LIMIT: + case PW_TOTAL_TIME_LIMIT: + case PW_ACTIVATION: + case PW_RADIUS_OPERATOR: + case PW_STRIP_USERNAME: + case PW_DAILY_TIME_LIMIT: + case PW_WEEKLY_TIME_LIMIT: + check_item = check_item->next; + continue; + } + /* + * See if this item is present in the request. + */ + + auth_item = request; + for (; auth_item != NULL; auth_item = auth_item->next) { + switch (check_item->attribute) { + case PW_PREFIX: + case PW_SUFFIX: + case PW_GROUP_NAME: + case PW_GROUP: + if (auth_item->attribute != + PW_USER_NAME) + continue; + /* Sizes are the same */ + strcpy(username, auth_item->strvalue); + case PW_HUNTGROUP_NAME: + break; + case PW_HINT: + if (auth_item->attribute != + check_item->attribute) + continue; + if (strcmp(check_item->strvalue, + auth_item->strvalue) != 0) + continue; + break; + default: + if (auth_item->attribute != + check_item->attribute) + continue; + } + break; + } + + if (auth_item == NULL) { + result = -1; + check_item = check_item->next; + continue; + } + + + /* + * OK it is present now compare them. + */ + + compare = 0; /* default result */ + switch(check_item->type) { + case PW_TYPE_STRING: + if (check_item->attribute == PW_PREFIX || + check_item->attribute == PW_SUFFIX) { + if (presufcmp(check_item, + auth_item->strvalue, username, + sizeof(username)) != 0) + result = -1; break; + } + else + if (check_item->attribute == PW_GROUP_NAME || + check_item->attribute == PW_GROUP) { + compare = groupcmp(check_item, username); + } + else + if (check_item->attribute == PW_HUNTGROUP_NAME){ + compare = (huntgroup_match(request, + check_item->strvalue) == 0); + DEBUG("COMPARE = %d", compare); break; + } + else + compare = strcmp(auth_item->strvalue, + check_item->strvalue); + break; + + case PW_TYPE_INTEGER: + if (check_item->attribute == PW_NAS_PORT_ID) { + compare = portcmp(check_item,auth_item); + break; + } + /*FALLTHRU*/ + case PW_TYPE_IPADDR: + compare = auth_item->lvalue - check_item->lvalue; + break; + + default: + result = -1; + break; + } + + switch (check_item->operator) + { + default: + case PW_OPERATOR_EQUAL: + if (compare != 0) result = -1; + break; + + case PW_OPERATOR_NOT_EQUAL: + if (compare == 0) result = -1; + break; + + case PW_OPERATOR_LESS_THAN: + if (compare >= 0) result = -1; + break; + + case PW_OPERATOR_GREATER_THAN: + if (compare <= 0) result = -1; + break; + + case PW_OPERATOR_LESS_EQUAL: + if (compare > 0) result = -1; + break; + + case PW_OPERATOR_GREATER_EQUAL: + if (compare < 0) result = -1; + break; + } + + /* Groups with OR logic for check_items: at least one check item + * must be satisfied. */ + if (strstr(check_item->group_name, "__OR__")) + { + DEBUG2("OR group %s: old_result = %i", check_item->group_name, result); + if (result == 0) old_result = 0; + result = 0; + } + + check_item = check_item->next; + + } + + for(i = *check; i; i = next) { + next = i->next; + if (strcmp(i->group_name,(*check)->group_name) != 0 && i->attribute == PW_AUTHTYPE && i->lvalue == PW_AUTHTYPE_ACCEPT) { + DEBUG2("REMOVING AUTH-TYPE: ACCEPT from %s", i->group_name); + last->next = next; + free(i); + } else + last = i; + } + + return result; + +} + + /* * Compare two pair lists. At least one of the check pairs * has to be present in the request. @@ -940,7 +1251,7 @@ VALUE_PAIR *reply_tmp = NULL; VALUE_PAIR **check_pairs; VALUE_PAIR **reply_pairs; - VALUE_PAIR *tmp; + VALUE_PAIR *tmp, *tmp2; VALUE_PAIR *request_pairs; int found = 0; #ifdef NT_DOMAIN_HACK @@ -1021,10 +1332,24 @@ /* * Fix dynamic IP address if needed. */ - if ((tmp = pairfind(*reply_pairs, PW_ADD_PORT_TO_IP_ADDRESS)) != NULL){ - if (tmp->lvalue != 0) { - tmp = pairfind(*reply_pairs, PW_FRAMED_IP_ADDRESS); - if (tmp) { + + tmp2 = *reply_pairs; + while (tmp2) { + if (tmp2->attribute != PW_ADD_PORT_TO_IP_ADDRESS) { + tmp2 = tmp2->next; + continue; + } + +// if ((tmp = pairfind(*reply_pairs, PW_ADD_PORT_TO_IP_ADDRESS)) != NULL){ + if (tmp2->lvalue != 0) { + tmp = *reply_pairs; + while (tmp) { + if (tmp->attribute != PW_FRAMED_IP_ADDRESS || strcmp(tmp->group_name, tmp2->group_name) != 0) { + tmp = tmp->next; + continue; + } + + if (tmp) { /* * FIXME: This only works because IP * numbers are stored in host order @@ -1034,10 +1359,13 @@ nas_port = ascend_port_number(nas_port); #endif tmp->lvalue += nas_port; + } + tmp = tmp->next; } } - pairdelete(reply_pairs, PW_ADD_PORT_TO_IP_ADDRESS); + tmp2 = tmp2->next; } + pairdelete(reply_pairs, PW_ADD_PORT_TO_IP_ADDRESS); /* * Remove server internal parameters. @@ -1177,6 +1505,7 @@ } /* No need for strncpy - same size */ strcpy(pair->name, attr->name); + strcpy(pair->group_name, "__USER_PARSE__"); pair->attribute = attr->value; pair->type = attr->type; pair->operator = operator; @@ -1258,6 +1587,7 @@ exit(1); } strcpy(pair2->name, "Add-Port-To-IP-Address"); + strcpy(pair2->group_name, pair->group_name); pair2->attribute = PW_ADD_PORT_TO_IP_ADDRESS; pair2->type = PW_TYPE_INTEGER; pair2->lvalue = x; Only in src-orlogic: files.c.orig diff -u -r src-old/mysql.c src-orlogic/mysql.c --- src-old/mysql.c Thu Jun 14 20:32:02 2001 +++ src-orlogic/mysql.c Sun May 12 00:41:52 2002 @@ -662,7 +662,7 @@ *************************************************************************/ int sql_userparse(VALUE_PAIR **first_pair, SQL_ROW row, int mode) { - int x, ufound, gfound, hintsfound; + int x, proxyfound, ufound, gfound, hintsfound; char *s; DICT_ATTR *attr = NULL; DICT_VALUE *dval; @@ -685,16 +685,29 @@ ufound = 0; gfound = 0; hintsfound = 0; + proxyfound = 0; + while (check) { if (attr->value == check->attribute) { + /* vi@maks.net: skip if the attribute was already received from proxy */ + if (check->source == PW_VP_PROXYDATA) + proxyfound = 1; if (check->source == PW_VP_HINTSDATA || mode == PW_VP_HINTSDATA) hintsfound = 1; if (check->source == PW_VP_USERDATA || mode == PW_VP_USERDATA) ufound = 1; - if (check->source == PW_VP_GROUPDATA || mode == PW_VP_GROUPDATA) + if (check->source == PW_VP_GROUPDATA || mode == PW_VP_GROUPDATA || mode == PW_VP_REALMDATA) gfound = 1; + + DEBUG2("Match, proxyfound = %u, ufound = %u, gfound = %u, mode = %d", proxyfound, ufound, gfound, mode); } + /* PROXY items ALWAYS replace user or group items */ + if (proxyfound && (ufound || gfound)) { + DEBUG2("attribute %s for user %s already received from proxy", row[2], row[1]); + return 1; + } + /* HINTS items ALWAYS replace user or group items */ if (hintsfound && (ufound || gfound)) { DEBUG2("attribute %s for user %s already in hints", row[2], row[1]); @@ -718,6 +731,7 @@ pair->type = attr->type; pair->operator = PW_OPERATOR_EQUAL; pair->source = mode; + strncpy(pair->group_name, row[1], sizeof(pair->group_name)); switch(pair->type) { #if defined( BINARY_FILTERS ) @@ -798,6 +812,7 @@ return -2; } strcpy(pair2->name, "Add-Port-To-IP-Address"); + strcpy(pair2->group_name, pair->group_name); pair2->attribute = PW_ADD_PORT_TO_IP_ADDRESS; pair2->type = PW_TYPE_INTEGER; pair2->lvalue = x; @@ -890,7 +905,8 @@ log(L_CONS|L_ERR, "out of memory"); return -1; } - sprintf(querystr, "SELECT %s.id, %s.GroupName, %s.Attribute, %s.Value FROM %s, %s WHERE %s AND %s.GroupName = %s.GroupName ORDER BY %s.id", table, table, table, table, table, sql->config->sql_usergroup_table, authstr, sql->config->sql_usergroup_table, table, table); +// sprintf(querystr, "SELECT %s.id, %s.GroupName, %s.Attribute, %s.Value FROM %s, %s WHERE %s AND %s.GroupName = %s.GroupName ORDER BY %s.id", table, table, table, table, table, sql->config->sql_usergroup_table, authstr, sql->config->sql_usergroup_table, table, table); + sprintf(querystr, "SELECT %s.id, %s.GroupName, %s.Attribute, %s.Value FROM %s, %s WHERE %s AND %s.GroupName = %s.GroupName ORDER BY Groupname", table, table, table, table, table, sql->config->sql_usergroup_table, authstr, sql->config->sql_usergroup_table, table); } else if (mode == PW_VP_REALMDATA) { if ((querystr = malloc(strlen(username) + (strlen(table) * 7) + (strlen(sql->config->sql_realmgroup_table) * 3) + 125)) == NULL) { @@ -898,7 +914,8 @@ log(L_CONS|L_ERR, "out of memory"); return -1; } - sprintf(querystr, "SELECT %s.id, %s.GroupName, %s.Attribute, %s.Value FROM %s, %s WHERE %s.RealmName = '%s' AND %s.GroupName = %s.GroupName ORDER BY %s.id", table, table, table, table, table, sql->config->sql_realmgroup_table, sql->config->sql_realmgroup_table, username, sql->config->sql_realmgroup_table, table, table); +// sprintf(querystr, "SELECT %s.id, %s.GroupName, %s.Attribute, %s.Value FROM %s, %s WHERE %s.RealmName = '%s' AND %s.GroupName = %s.GroupName ORDER BY %s.id", table, table, table, table, table, sql->config->sql_realmgroup_table, sql->config->sql_realmgroup_table, username, sql->config->sql_realmgroup_table, table, table); + sprintf(querystr, "SELECT %s.id, %s.GroupName, %s.Attribute, %s.Value FROM %s, %s WHERE %s.RealmName = '%s' AND %s.GroupName = %s.GroupName ORDER BY GroupName", table, table, table, table, table, sql->config->sql_realmgroup_table, sql->config->sql_realmgroup_table, username, sql->config->sql_realmgroup_table, table); } else if (mode == PW_VP_HINTSDATA) { if ((querystr = malloc(strlen(username) + strlen(table) + 125)) == NULL) { free(authstr); Only in src-orlogic: mysql.c.orig diff -u -r src-old/proxy.c src-orlogic/proxy.c --- src-old/proxy.c Wed Jun 6 20:21:33 2001 +++ src-orlogic/proxy.c Sat May 11 23:43:16 2002 @@ -591,6 +591,10 @@ x = NULL; prev = NULL; + for (vp = authreq->request; vp; vp = vp->next) + /* vi@maks.net: set the source to PW_VP_PROXYDATA */ + vp->source = PW_VP_PROXYDATA; + for (vp = authreq->proxy_pairs; vp; vp = vp->next) { if (vp->attribute == PW_PROXY_STATE) { prev = x; diff -u -r src-old/radius.h src-orlogic/radius.h --- src-old/radius.h Tue Nov 21 20:35:27 2000 +++ src-orlogic/radius.h Sat May 11 23:19:39 2002 @@ -63,6 +63,7 @@ #define PW_VP_GROUPDATA 2 #define PW_VP_REALMDATA 3 #define PW_VP_HINTSDATA 4 +#define PW_VP_PROXYDATA 5 #define PW_AUTHENTICATION_REQUEST 1 #define PW_AUTHENTICATION_ACK 2 diff -u -r src-old/radiusd.h src-orlogic/radiusd.h --- src-old/radiusd.h Tue Jun 12 04:31:23 2001 +++ src-orlogic/radiusd.h Fri Nov 9 10:54:37 2001 @@ -69,6 +69,7 @@ int source; char strvalue[AUTH_STRING_LEN]; struct value_pair *next; + char group_name[30]; } VALUE_PAIR; typedef struct auth_req { @@ -274,6 +275,7 @@ struct passwd *rad_getpwnam(char *); VALUE_PAIR *pairfind(VALUE_PAIR *, int); void pairdelete(VALUE_PAIR **, int); +void pairdelete_group(VALUE_PAIR **, char *group); void pairadd(VALUE_PAIR **, VALUE_PAIR *); void authfree(AUTH_REQ *authreq); #if defined (sun) && defined(__svr4__) || defined(__hpux__) || defined(aix) @@ -304,6 +306,7 @@ char *auth_name(AUTH_REQ *authreq, int do_cid); int read_config_files(SQLSOCK *socket); int paircmp(VALUE_PAIR *request, VALUE_PAIR *check); +int paircmp_group(VALUE_PAIR *request, VALUE_PAIR **check, VALUE_PAIR **reply_pairs); int presufcmp(VALUE_PAIR *check, char *name, char *rest, int rl); void pairmove(VALUE_PAIR **to, VALUE_PAIR **from); void pairmove2(VALUE_PAIR **to, VALUE_PAIR **from, int attr); Only in src-orlogic: radiusd.h.orig diff -u -r src-old/util.c src-orlogic/util.c --- src-old/util.c Wed Jun 6 20:21:33 2001 +++ src-orlogic/util.c Fri Nov 9 10:54:37 2001 @@ -225,7 +225,7 @@ /* - * Find the pair with the mathing attribute + * Find the pair with the matching attribute */ VALUE_PAIR * pairfind(VALUE_PAIR *first, int attr) { @@ -236,7 +236,7 @@ /* - * Delete the pair(s) with the mathing attribute + * Delete the pair(s) with the matching attribute */ void pairdelete(VALUE_PAIR **first, int attr) { @@ -249,6 +249,28 @@ last->next = next; else *first = next; + free(i); + } else + last = i; + } +} + +/* + * Delete the pair(s) with the matching group + */ +void pairdelete_group(VALUE_PAIR **first, char *group) +{ + VALUE_PAIR *i, *next, *last = NULL; + + for(i = *first; i; i = next) { + next = i->next; +/* DEBUG2("%s %s",i->group_name, group); */ + if (strncmp(i->group_name, group, sizeof (i->group_name)-1) == 0) { + if (last) + last->next = next; + else { + *first = next; + } free(i); } else last = i;