diff options
Diffstat (limited to 'src/drivers/driver_wext.c')
-rw-r--r-- | src/drivers/driver_wext.c | 394 |
1 files changed, 347 insertions, 47 deletions
diff --git a/src/drivers/driver_wext.c b/src/drivers/driver_wext.c index 2614f23093fac..9733e015638d5 100644 --- a/src/drivers/driver_wext.c +++ b/src/drivers/driver_wext.c @@ -2,14 +2,8 @@ * Driver interaction with generic Linux Wireless Extensions * Copyright (c) 2003-2010, Jouni Malinen <j@w1.fi> * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 2 as - * published by the Free Software Foundation. - * - * Alternatively, this software may be distributed under the terms of BSD - * license. - * - * See README and COPYING for more details. + * This software may be distributed under the terms of the BSD license. + * See README for more details. * * This file implements a driver interface for the Linux Wireless Extensions. * When used with WE-18 or newer, this interface can be used as-is with number @@ -20,10 +14,12 @@ #include "includes.h" #include <sys/ioctl.h> +#include <sys/types.h> #include <sys/stat.h> +#include <fcntl.h> #include <net/if_arp.h> -#include "wireless_copy.h" +#include "linux_wext.h" #include "common.h" #include "eloop.h" #include "common/ieee802_11_defs.h" @@ -31,9 +27,13 @@ #include "priv_netlink.h" #include "netlink.h" #include "linux_ioctl.h" +#include "rfkill.h" #include "driver.h" #include "driver_wext.h" +#ifdef ANDROID +#include "android_drv.h" +#endif /* ANDROID */ static int wpa_driver_wext_flush_pmkid(void *priv); static int wpa_driver_wext_get_range(void *priv); @@ -299,6 +299,14 @@ wpa_driver_wext_event_wireless_custom(void *ctx, char *custom) } wpa_supplicant_event(ctx, EVENT_STKSTART, &data); #endif /* CONFIG_PEERKEY */ +#ifdef ANDROID + } else if (os_strncmp(custom, "STOP", 4) == 0) { + wpa_msg(ctx, MSG_INFO, WPA_EVENT_DRIVER_STATE "STOPPED"); + } else if (os_strncmp(custom, "START", 5) == 0) { + wpa_msg(ctx, MSG_INFO, WPA_EVENT_DRIVER_STATE "STARTED"); + } else if (os_strncmp(custom, "HANG", 4) == 0) { + wpa_msg(ctx, MSG_INFO, WPA_EVENT_DRIVER_STATE "HANGED"); +#endif /* ANDROID */ } } @@ -561,10 +569,28 @@ static void wpa_driver_wext_event_link(struct wpa_driver_wext_data *drv, del ? "removed" : "added"); if (os_strcmp(drv->ifname, event.interface_status.ifname) == 0) { - if (del) + if (del) { + if (drv->if_removed) { + wpa_printf(MSG_DEBUG, "WEXT: if_removed " + "already set - ignore event"); + return; + } drv->if_removed = 1; - else + } else { + if (if_nametoindex(drv->ifname) == 0) { + wpa_printf(MSG_DEBUG, "WEXT: Interface %s " + "does not exist - ignore " + "RTM_NEWLINK", + drv->ifname); + return; + } + if (!drv->if_removed) { + wpa_printf(MSG_DEBUG, "WEXT: if_removed " + "already cleared - ignore event"); + return; + } drv->if_removed = 0; + } } wpa_supplicant_event(drv->ctx, EVENT_INTERFACE_STATUS, &event); @@ -620,6 +646,7 @@ static void wpa_driver_wext_event_rtm_newlink(void *ctx, struct ifinfomsg *ifi, struct wpa_driver_wext_data *drv = ctx; int attrlen, rta_len; struct rtattr *attr; + char namebuf[IFNAMSIZ]; if (!wpa_driver_wext_own_ifindex(drv, ifi->ifi_index, buf, len)) { wpa_printf(MSG_DEBUG, "Ignore event for foreign ifindex %d", @@ -634,6 +661,35 @@ static void wpa_driver_wext_event_rtm_newlink(void *ctx, struct ifinfomsg *ifi, (ifi->ifi_flags & IFF_RUNNING) ? "[RUNNING]" : "", (ifi->ifi_flags & IFF_LOWER_UP) ? "[LOWER_UP]" : "", (ifi->ifi_flags & IFF_DORMANT) ? "[DORMANT]" : ""); + + if (!drv->if_disabled && !(ifi->ifi_flags & IFF_UP)) { + wpa_printf(MSG_DEBUG, "WEXT: Interface down"); + drv->if_disabled = 1; + wpa_supplicant_event(drv->ctx, EVENT_INTERFACE_DISABLED, NULL); + } + + if (drv->if_disabled && (ifi->ifi_flags & IFF_UP)) { + if (if_indextoname(ifi->ifi_index, namebuf) && + linux_iface_up(drv->ioctl_sock, drv->ifname) == 0) { + wpa_printf(MSG_DEBUG, "WEXT: Ignore interface up " + "event since interface %s is down", + namebuf); + } else if (if_nametoindex(drv->ifname) == 0) { + wpa_printf(MSG_DEBUG, "WEXT: Ignore interface up " + "event since interface %s does not exist", + drv->ifname); + } else if (drv->if_removed) { + wpa_printf(MSG_DEBUG, "WEXT: Ignore interface up " + "event since interface %s is marked " + "removed", drv->ifname); + } else { + wpa_printf(MSG_DEBUG, "WEXT: Interface up"); + drv->if_disabled = 0; + wpa_supplicant_event(drv->ctx, EVENT_INTERFACE_ENABLED, + NULL); + } + } + /* * Some drivers send the association event before the operup event--in * this case, lifting operstate in wpa_driver_wext_set_operstate() @@ -687,6 +743,62 @@ static void wpa_driver_wext_event_rtm_dellink(void *ctx, struct ifinfomsg *ifi, } +static void wpa_driver_wext_rfkill_blocked(void *ctx) +{ + wpa_printf(MSG_DEBUG, "WEXT: RFKILL blocked"); + /* + * This may be for any interface; use ifdown event to disable + * interface. + */ +} + + +static void wpa_driver_wext_rfkill_unblocked(void *ctx) +{ + struct wpa_driver_wext_data *drv = ctx; + wpa_printf(MSG_DEBUG, "WEXT: RFKILL unblocked"); + if (linux_set_iface_flags(drv->ioctl_sock, drv->ifname, 1)) { + wpa_printf(MSG_DEBUG, "WEXT: Could not set interface UP " + "after rfkill unblock"); + return; + } + /* rtnetlink ifup handler will report interface as enabled */ +} + + +static void wext_get_phy_name(struct wpa_driver_wext_data *drv) +{ + /* Find phy (radio) to which this interface belongs */ + char buf[90], *pos; + int f, rv; + + drv->phyname[0] = '\0'; + snprintf(buf, sizeof(buf) - 1, "/sys/class/net/%s/phy80211/name", + drv->ifname); + f = open(buf, O_RDONLY); + if (f < 0) { + wpa_printf(MSG_DEBUG, "Could not open file %s: %s", + buf, strerror(errno)); + return; + } + + rv = read(f, drv->phyname, sizeof(drv->phyname) - 1); + close(f); + if (rv < 0) { + wpa_printf(MSG_DEBUG, "Could not read file %s: %s", + buf, strerror(errno)); + return; + } + + drv->phyname[rv] = '\0'; + pos = os_strchr(drv->phyname, '\n'); + if (pos) + *pos = '\0'; + wpa_printf(MSG_DEBUG, "wext: interface %s phy: %s", + drv->ifname, drv->phyname); +} + + /** * wpa_driver_wext_init - Initialize WE driver interface * @ctx: context to be used when calling wpa_supplicant functions, @@ -698,6 +810,7 @@ void * wpa_driver_wext_init(void *ctx, const char *ifname) { struct wpa_driver_wext_data *drv; struct netlink_config *cfg; + struct rfkill_config *rcfg; char path[128]; struct stat buf; @@ -711,6 +824,7 @@ void * wpa_driver_wext_init(void *ctx, const char *ifname) if (stat(path, &buf) == 0) { wpa_printf(MSG_DEBUG, "WEXT: cfg80211-based driver detected"); drv->cfg80211 = 1; + wext_get_phy_name(drv); } drv->ioctl_sock = socket(PF_INET, SOCK_DGRAM, 0); @@ -731,8 +845,27 @@ void * wpa_driver_wext_init(void *ctx, const char *ifname) goto err2; } + rcfg = os_zalloc(sizeof(*rcfg)); + if (rcfg == NULL) + goto err3; + rcfg->ctx = drv; + os_strlcpy(rcfg->ifname, ifname, sizeof(rcfg->ifname)); + rcfg->blocked_cb = wpa_driver_wext_rfkill_blocked; + rcfg->unblocked_cb = wpa_driver_wext_rfkill_unblocked; + drv->rfkill = rfkill_init(rcfg); + if (drv->rfkill == NULL) { + wpa_printf(MSG_DEBUG, "WEXT: RFKILL status not available"); + os_free(rcfg); + } + drv->mlme_sock = -1; +#ifdef ANDROID + drv->errors = 0; + drv->driver_is_started = TRUE; + drv->bgscan_enabled = 0; +#endif /* ANDROID */ + if (wpa_driver_wext_finish_drv_init(drv) < 0) goto err3; @@ -741,6 +874,7 @@ void * wpa_driver_wext_init(void *ctx, const char *ifname) return drv; err3: + rfkill_deinit(drv->rfkill); netlink_deinit(drv->netlink); err2: close(drv->ioctl_sock); @@ -750,10 +884,29 @@ err1: } +static void wpa_driver_wext_send_rfkill(void *eloop_ctx, void *timeout_ctx) +{ + wpa_supplicant_event(timeout_ctx, EVENT_INTERFACE_DISABLED, NULL); +} + + static int wpa_driver_wext_finish_drv_init(struct wpa_driver_wext_data *drv) { - if (linux_set_iface_flags(drv->ioctl_sock, drv->ifname, 1) < 0) - return -1; + int send_rfkill_event = 0; + + if (linux_set_iface_flags(drv->ioctl_sock, drv->ifname, 1) < 0) { + if (rfkill_is_blocked(drv->rfkill)) { + wpa_printf(MSG_DEBUG, "WEXT: Could not yet enable " + "interface '%s' due to rfkill", + drv->ifname); + drv->if_disabled = 1; + send_rfkill_event = 1; + } else { + wpa_printf(MSG_ERROR, "WEXT: Could not set " + "interface '%s' UP", drv->ifname); + return -1; + } + } /* * Make sure that the driver does not have any obsolete PMKID entries. @@ -795,6 +948,11 @@ static int wpa_driver_wext_finish_drv_init(struct wpa_driver_wext_data *drv) netlink_send_oper_ifla(drv->netlink, drv->ifindex, 1, IF_OPER_DORMANT); + if (send_rfkill_event) { + eloop_register_timeout(0, 0, wpa_driver_wext_send_rfkill, + drv, drv->ctx); + } + return 0; } @@ -822,6 +980,7 @@ void wpa_driver_wext_deinit(void *priv) netlink_send_oper_ifla(drv->netlink, drv->ifindex, 0, IF_OPER_UP); netlink_deinit(drv->netlink); + rfkill_deinit(drv->rfkill); if (drv->mlme_sock >= 0) eloop_unregister_read_sock(drv->mlme_sock); @@ -894,7 +1053,7 @@ int wpa_driver_wext_scan(void *priv, struct wpa_driver_scan_params *params) /* Not all drivers generate "scan completed" wireless event, so try to * read results after a timeout. */ - timeout = 5; + timeout = 10; if (drv->scan_complete_events) { /* * The driver seems to deliver SIOCGIWSCAN events to notify @@ -1040,7 +1199,8 @@ static void wext_get_scan_freq(struct iw_event *iwe, } -static void wext_get_scan_qual(struct iw_event *iwe, +static void wext_get_scan_qual(struct wpa_driver_wext_data *drv, + struct iw_event *iwe, struct wext_scan_data *res) { res->res.qual = iwe->u.qual.qual; @@ -1054,6 +1214,14 @@ static void wext_get_scan_qual(struct iw_event *iwe, res->res.flags |= WPA_SCAN_NOISE_INVALID; if (iwe->u.qual.updated & IW_QUAL_DBM) res->res.flags |= WPA_SCAN_LEVEL_DBM; + if ((iwe->u.qual.updated & IW_QUAL_DBM) || + ((iwe->u.qual.level != 0) && + (iwe->u.qual.level > drv->max_level))) { + if (iwe->u.qual.level >= 64) + res->res.level -= 0x100; + if (iwe->u.qual.noise >= 64) + res->res.noise -= 0x100; + } } @@ -1246,8 +1414,8 @@ static void wpa_driver_wext_add_scan_entry(struct wpa_scan_results *res, if (data->ie) os_memcpy(pos, data->ie, data->ie_len); - tmp = os_realloc(res->res, - (res->num + 1) * sizeof(struct wpa_scan_res *)); + tmp = os_realloc_array(res->res, res->num + 1, + sizeof(struct wpa_scan_res *)); if (tmp == NULL) { os_free(r); return; @@ -1255,7 +1423,7 @@ static void wpa_driver_wext_add_scan_entry(struct wpa_scan_results *res, tmp[res->num++] = r; res->res = tmp; } - + /** * wpa_driver_wext_get_scan_results - Fetch the latest scan results @@ -1265,7 +1433,7 @@ static void wpa_driver_wext_add_scan_entry(struct wpa_scan_results *res, struct wpa_scan_results * wpa_driver_wext_get_scan_results(void *priv) { struct wpa_driver_wext_data *drv = priv; - size_t ap_num = 0, len; + size_t len; int first; u8 *res_buf; struct iw_event iwe_buf, *iwe = &iwe_buf; @@ -1277,7 +1445,6 @@ struct wpa_scan_results * wpa_driver_wext_get_scan_results(void *priv) if (res_buf == NULL) return NULL; - ap_num = 0; first = 1; res = os_zalloc(sizeof(*res)); @@ -1329,7 +1496,7 @@ struct wpa_scan_results * wpa_driver_wext_get_scan_results(void *priv) wext_get_scan_freq(iwe, &data); break; case IWEVQUAL: - wext_get_scan_qual(iwe, &data); + wext_get_scan_qual(drv, iwe, &data); break; case SIOCGIWENCODE: wext_get_scan_encode(iwe, &data); @@ -1408,6 +1575,7 @@ static int wpa_driver_wext_get_range(void *priv) } drv->capa.enc |= WPA_DRIVER_CAPA_ENC_WEP40 | WPA_DRIVER_CAPA_ENC_WEP104; + drv->capa.enc |= WPA_DRIVER_CAPA_ENC_WEP128; if (range->enc_capa & IW_ENC_CAPA_CIPHER_TKIP) drv->capa.enc |= WPA_DRIVER_CAPA_ENC_TKIP; if (range->enc_capa & IW_ENC_CAPA_CIPHER_CCMP) @@ -1427,6 +1595,8 @@ static int wpa_driver_wext_get_range(void *priv) "assuming WPA is not supported"); } + drv->max_level = range->max_qual.level; + os_free(range); return 0; } @@ -1498,8 +1668,7 @@ static int wpa_driver_wext_set_key_ext(void *priv, enum wpa_alg alg, iwr.u.encoding.pointer = (caddr_t) ext; iwr.u.encoding.length = sizeof(*ext) + key_len; - if (addr == NULL || - os_memcmp(addr, "\xff\xff\xff\xff\xff\xff", ETH_ALEN) == 0) + if (addr == NULL || is_broadcast_ether_addr(addr)) ext->ext_flags |= IW_ENCODE_EXT_GROUP_KEY; if (set_tx) ext->ext_flags |= IW_ENCODE_EXT_SET_TX_KEY; @@ -1702,8 +1871,10 @@ static void wpa_driver_wext_disconnect(struct wpa_driver_wext_data *drv) { struct iwreq iwr; const u8 null_bssid[ETH_ALEN] = { 0, 0, 0, 0, 0, 0 }; +#ifndef ANDROID u8 ssid[32]; int i; +#endif /* ANDROID */ /* * Only force-disconnect when the card is in infrastructure mode, @@ -1718,32 +1889,39 @@ static void wpa_driver_wext_disconnect(struct wpa_driver_wext_data *drv) } if (iwr.u.mode == IW_MODE_INFRA) { + /* Clear the BSSID selection */ + if (wpa_driver_wext_set_bssid(drv, null_bssid) < 0) { + wpa_printf(MSG_DEBUG, "WEXT: Failed to clear BSSID " + "selection on disconnect"); + } + +#ifndef ANDROID if (drv->cfg80211) { /* * cfg80211 supports SIOCSIWMLME commands, so there is * no need for the random SSID hack, but clear the - * BSSID and SSID. + * SSID. */ - if (wpa_driver_wext_set_bssid(drv, null_bssid) < 0 || - wpa_driver_wext_set_ssid(drv, (u8 *) "", 0) < 0) { + if (wpa_driver_wext_set_ssid(drv, (u8 *) "", 0) < 0) { wpa_printf(MSG_DEBUG, "WEXT: Failed to clear " - "to disconnect"); + "SSID on disconnect"); } return; } + /* - * Clear the BSSID selection and set a random SSID to make sure - * the driver will not be trying to associate with something - * even if it does not understand SIOCSIWMLME commands (or - * tries to associate automatically after deauth/disassoc). + * Set a random SSID to make sure the driver will not be trying + * to associate with something even if it does not understand + * SIOCSIWMLME commands (or tries to associate automatically + * after deauth/disassoc). */ for (i = 0; i < 32; i++) ssid[i] = rand() & 0xFF; - if (wpa_driver_wext_set_bssid(drv, null_bssid) < 0 || - wpa_driver_wext_set_ssid(drv, ssid, 32) < 0) { + if (wpa_driver_wext_set_ssid(drv, ssid, 32) < 0) { wpa_printf(MSG_DEBUG, "WEXT: Failed to set bogus " - "BSSID/SSID to disconnect"); + "SSID to disconnect"); } +#endif /* ANDROID */ } } @@ -1760,18 +1938,6 @@ static int wpa_driver_wext_deauthenticate(void *priv, const u8 *addr, } -static int wpa_driver_wext_disassociate(void *priv, const u8 *addr, - int reason_code) -{ - struct wpa_driver_wext_data *drv = priv; - int ret; - wpa_printf(MSG_DEBUG, "%s", __FUNCTION__); - ret = wpa_driver_wext_mlme(drv, addr, IW_MLME_DISASSOC, reason_code); - wpa_driver_wext_disconnect(drv); - return ret; -} - - static int wpa_driver_wext_set_gen_ie(void *priv, const u8 *ie, size_t ie_len) { @@ -2167,6 +2333,136 @@ int wpa_driver_wext_get_version(struct wpa_driver_wext_data *drv) } +static const char * wext_get_radio_name(void *priv) +{ + struct wpa_driver_wext_data *drv = priv; + return drv->phyname; +} + + +#ifdef ANDROID + +static int android_wext_cmd(struct wpa_driver_wext_data *drv, const char *cmd) +{ + struct iwreq iwr; + char buf[MAX_DRV_CMD_SIZE]; + int ret; + + os_memset(&iwr, 0, sizeof(iwr)); + os_strlcpy(iwr.ifr_name, drv->ifname, IFNAMSIZ); + + os_memset(buf, 0, sizeof(buf)); + os_strlcpy(buf, cmd, sizeof(buf)); + + iwr.u.data.pointer = buf; + iwr.u.data.length = sizeof(buf); + + ret = ioctl(drv->ioctl_sock, SIOCSIWPRIV, &iwr); + + if (ret < 0) { + wpa_printf(MSG_ERROR, "%s failed (%d): %s", __func__, ret, + cmd); + drv->errors++; + if (drv->errors > DRV_NUMBER_SEQUENTIAL_ERRORS) { + drv->errors = 0; + wpa_msg(drv->ctx, MSG_INFO, WPA_EVENT_DRIVER_STATE + "HANGED"); + } + return ret; + } + + drv->errors = 0; + return 0; +} + + +static int wext_sched_scan(void *priv, struct wpa_driver_scan_params *params, + u32 interval) +{ + struct wpa_driver_wext_data *drv = priv; + struct iwreq iwr; + int ret = 0, i = 0, bp; + char buf[WEXT_PNO_MAX_COMMAND_SIZE]; + + bp = WEXT_PNOSETUP_HEADER_SIZE; + os_memcpy(buf, WEXT_PNOSETUP_HEADER, bp); + buf[bp++] = WEXT_PNO_TLV_PREFIX; + buf[bp++] = WEXT_PNO_TLV_VERSION; + buf[bp++] = WEXT_PNO_TLV_SUBVERSION; + buf[bp++] = WEXT_PNO_TLV_RESERVED; + + while (i < WEXT_PNO_AMOUNT && (size_t) i < params->num_ssids) { + /* + * Check that there is enough space needed for 1 more SSID, the + * other sections and null termination. + */ + if ((bp + WEXT_PNO_SSID_HEADER_SIZE + IW_ESSID_MAX_SIZE + + WEXT_PNO_NONSSID_SECTIONS_SIZE + 1) >= (int) sizeof(buf)) + break; + + wpa_hexdump_ascii(MSG_DEBUG, "For PNO Scan", + params->ssids[i].ssid, + params->ssids[i].ssid_len); + buf[bp++] = WEXT_PNO_SSID_SECTION; + buf[bp++] = params->ssids[i].ssid_len; + os_memcpy(&buf[bp], params->ssids[i].ssid, + params->ssids[i].ssid_len); + bp += params->ssids[i].ssid_len; + i++; + } + + buf[bp++] = WEXT_PNO_SCAN_INTERVAL_SECTION; + /* TODO: consider using interval parameter (interval in msec) instead + * of hardcoded value here */ + os_snprintf(&buf[bp], WEXT_PNO_SCAN_INTERVAL_LENGTH + 1, "%x", + WEXT_PNO_SCAN_INTERVAL); + bp += WEXT_PNO_SCAN_INTERVAL_LENGTH; + + buf[bp++] = WEXT_PNO_REPEAT_SECTION; + os_snprintf(&buf[bp], WEXT_PNO_REPEAT_LENGTH + 1, "%x", + WEXT_PNO_REPEAT); + bp += WEXT_PNO_REPEAT_LENGTH; + + buf[bp++] = WEXT_PNO_MAX_REPEAT_SECTION; + os_snprintf(&buf[bp], WEXT_PNO_MAX_REPEAT_LENGTH + 1, "%x", + WEXT_PNO_MAX_REPEAT); + bp += WEXT_PNO_MAX_REPEAT_LENGTH + 1; + + os_memset(&iwr, 0, sizeof(iwr)); + os_strncpy(iwr.ifr_name, drv->ifname, IFNAMSIZ); + iwr.u.data.pointer = buf; + iwr.u.data.length = bp; + + ret = ioctl(drv->ioctl_sock, SIOCSIWPRIV, &iwr); + if (ret < 0) { + wpa_printf(MSG_ERROR, "ioctl[SIOCSIWPRIV] (pnosetup): %d", + ret); + drv->errors++; + if (drv->errors > DRV_NUMBER_SEQUENTIAL_ERRORS) { + drv->errors = 0; + wpa_msg(drv->ctx, MSG_INFO, + WPA_EVENT_DRIVER_STATE "HANGED"); + } + return ret; + } + + drv->errors = 0; + drv->bgscan_enabled = 1; + + return android_wext_cmd(drv, "PNOFORCE 1"); +} + + +static int wext_stop_sched_scan(void *priv) +{ + struct wpa_driver_wext_data *drv = priv; + drv->bgscan_enabled = 0; + return android_wext_cmd(drv, "PNOFORCE 0"); +} + +#endif /* ANDROID */ + + const struct wpa_driver_ops wpa_driver_wext_ops = { .name = "wext", .desc = "Linux wireless extensions (generic)", @@ -2177,7 +2473,6 @@ const struct wpa_driver_ops wpa_driver_wext_ops = { .scan2 = wpa_driver_wext_scan, .get_scan_results2 = wpa_driver_wext_get_scan_results, .deauthenticate = wpa_driver_wext_deauthenticate, - .disassociate = wpa_driver_wext_disassociate, .associate = wpa_driver_wext_associate, .init = wpa_driver_wext_init, .deinit = wpa_driver_wext_deinit, @@ -2186,4 +2481,9 @@ const struct wpa_driver_ops wpa_driver_wext_ops = { .flush_pmkid = wpa_driver_wext_flush_pmkid, .get_capa = wpa_driver_wext_get_capa, .set_operstate = wpa_driver_wext_set_operstate, + .get_radio_name = wext_get_radio_name, +#ifdef ANDROID + .sched_scan = wext_sched_scan, + .stop_sched_scan = wext_stop_sched_scan, +#endif /* ANDROID */ }; |