| /* |
| * UPnP WPS Device - Web connections |
| * Copyright (c) 2000-2003 Intel Corporation |
| * Copyright (c) 2006-2007 Sony Corporation |
| * Copyright (c) 2008-2009 Atheros Communications |
| * Copyright (c) 2009, Jouni Malinen <j@w1.fi> |
| * |
| * See wps_upnp.c for more details on licensing and code history. |
| */ |
| |
| #include "includes.h" |
| |
| #include "common.h" |
| #include "base64.h" |
| #include "uuid.h" |
| #include "httpread.h" |
| #include "http_server.h" |
| #include "wps_i.h" |
| #include "wps_upnp.h" |
| #include "wps_upnp_i.h" |
| #include "upnp_xml.h" |
| |
| /*************************************************************************** |
| * Web connections (we serve pages of info about ourselves, handle |
| * requests, etc. etc.). |
| **************************************************************************/ |
| |
| #define WEB_CONNECTION_TIMEOUT_SEC 30 /* Drop web connection after t.o. */ |
| #define WEB_CONNECTION_MAX_READ 8000 /* Max we'll read for TCP request */ |
| #define MAX_WEB_CONNECTIONS 10 /* max simultaneous web connects */ |
| |
| |
| static const char *urn_wfawlanconfig = |
| "urn:schemas-wifialliance-org:service:WFAWLANConfig:1"; |
| static const char *http_server_hdr = |
| "Server: unspecified, UPnP/1.0, unspecified\r\n"; |
| static const char *http_connection_close = |
| "Connection: close\r\n"; |
| |
| /* |
| * "Files" that we serve via HTTP. The format of these files is given by |
| * WFA WPS specifications. Extra white space has been removed to save space. |
| */ |
| |
| static const char wps_scpd_xml[] = |
| "<?xml version=\"1.0\"?>\n" |
| "<scpd xmlns=\"urn:schemas-upnp-org:service-1-0\">\n" |
| "<specVersion><major>1</major><minor>0</minor></specVersion>\n" |
| "<actionList>\n" |
| "<action>\n" |
| "<name>GetDeviceInfo</name>\n" |
| "<argumentList>\n" |
| "<argument>\n" |
| "<name>NewDeviceInfo</name>\n" |
| "<direction>out</direction>\n" |
| "<relatedStateVariable>DeviceInfo</relatedStateVariable>\n" |
| "</argument>\n" |
| "</argumentList>\n" |
| "</action>\n" |
| "<action>\n" |
| "<name>PutMessage</name>\n" |
| "<argumentList>\n" |
| "<argument>\n" |
| "<name>NewInMessage</name>\n" |
| "<direction>in</direction>\n" |
| "<relatedStateVariable>InMessage</relatedStateVariable>\n" |
| "</argument>\n" |
| "<argument>\n" |
| "<name>NewOutMessage</name>\n" |
| "<direction>out</direction>\n" |
| "<relatedStateVariable>OutMessage</relatedStateVariable>\n" |
| "</argument>\n" |
| "</argumentList>\n" |
| "</action>\n" |
| "<action>\n" |
| "<name>PutWLANResponse</name>\n" |
| "<argumentList>\n" |
| "<argument>\n" |
| "<name>NewMessage</name>\n" |
| "<direction>in</direction>\n" |
| "<relatedStateVariable>Message</relatedStateVariable>\n" |
| "</argument>\n" |
| "<argument>\n" |
| "<name>NewWLANEventType</name>\n" |
| "<direction>in</direction>\n" |
| "<relatedStateVariable>WLANEventType</relatedStateVariable>\n" |
| "</argument>\n" |
| "<argument>\n" |
| "<name>NewWLANEventMAC</name>\n" |
| "<direction>in</direction>\n" |
| "<relatedStateVariable>WLANEventMAC</relatedStateVariable>\n" |
| "</argument>\n" |
| "</argumentList>\n" |
| "</action>\n" |
| "<action>\n" |
| "<name>SetSelectedRegistrar</name>\n" |
| "<argumentList>\n" |
| "<argument>\n" |
| "<name>NewMessage</name>\n" |
| "<direction>in</direction>\n" |
| "<relatedStateVariable>Message</relatedStateVariable>\n" |
| "</argument>\n" |
| "</argumentList>\n" |
| "</action>\n" |
| "</actionList>\n" |
| "<serviceStateTable>\n" |
| "<stateVariable sendEvents=\"no\">\n" |
| "<name>Message</name>\n" |
| "<dataType>bin.base64</dataType>\n" |
| "</stateVariable>\n" |
| "<stateVariable sendEvents=\"no\">\n" |
| "<name>InMessage</name>\n" |
| "<dataType>bin.base64</dataType>\n" |
| "</stateVariable>\n" |
| "<stateVariable sendEvents=\"no\">\n" |
| "<name>OutMessage</name>\n" |
| "<dataType>bin.base64</dataType>\n" |
| "</stateVariable>\n" |
| "<stateVariable sendEvents=\"no\">\n" |
| "<name>DeviceInfo</name>\n" |
| "<dataType>bin.base64</dataType>\n" |
| "</stateVariable>\n" |
| "<stateVariable sendEvents=\"yes\">\n" |
| "<name>APStatus</name>\n" |
| "<dataType>ui1</dataType>\n" |
| "</stateVariable>\n" |
| "<stateVariable sendEvents=\"yes\">\n" |
| "<name>STAStatus</name>\n" |
| "<dataType>ui1</dataType>\n" |
| "</stateVariable>\n" |
| "<stateVariable sendEvents=\"yes\">\n" |
| "<name>WLANEvent</name>\n" |
| "<dataType>bin.base64</dataType>\n" |
| "</stateVariable>\n" |
| "<stateVariable sendEvents=\"no\">\n" |
| "<name>WLANEventType</name>\n" |
| "<dataType>ui1</dataType>\n" |
| "</stateVariable>\n" |
| "<stateVariable sendEvents=\"no\">\n" |
| "<name>WLANEventMAC</name>\n" |
| "<dataType>string</dataType>\n" |
| "</stateVariable>\n" |
| "<stateVariable sendEvents=\"no\">\n" |
| "<name>WLANResponse</name>\n" |
| "<dataType>bin.base64</dataType>\n" |
| "</stateVariable>\n" |
| "</serviceStateTable>\n" |
| "</scpd>\n" |
| ; |
| |
| |
| static const char *wps_device_xml_prefix = |
| "<?xml version=\"1.0\"?>\n" |
| "<root xmlns=\"urn:schemas-upnp-org:device-1-0\">\n" |
| "<specVersion>\n" |
| "<major>1</major>\n" |
| "<minor>0</minor>\n" |
| "</specVersion>\n" |
| "<device>\n" |
| "<deviceType>urn:schemas-wifialliance-org:device:WFADevice:1" |
| "</deviceType>\n"; |
| |
| static const char *wps_device_xml_postfix = |
| "<serviceList>\n" |
| "<service>\n" |
| "<serviceType>urn:schemas-wifialliance-org:service:WFAWLANConfig:1" |
| "</serviceType>\n" |
| "<serviceId>urn:wifialliance-org:serviceId:WFAWLANConfig1</serviceId>" |
| "\n" |
| "<SCPDURL>" UPNP_WPS_SCPD_XML_FILE "</SCPDURL>\n" |
| "<controlURL>" UPNP_WPS_DEVICE_CONTROL_FILE "</controlURL>\n" |
| "<eventSubURL>" UPNP_WPS_DEVICE_EVENT_FILE "</eventSubURL>\n" |
| "</service>\n" |
| "</serviceList>\n" |
| "</device>\n" |
| "</root>\n"; |
| |
| |
| /* format_wps_device_xml -- produce content of "file" wps_device.xml |
| * (UPNP_WPS_DEVICE_XML_FILE) |
| */ |
| static void format_wps_device_xml(struct upnp_wps_device_interface *iface, |
| struct upnp_wps_device_sm *sm, |
| struct wpabuf *buf) |
| { |
| const char *s; |
| char uuid_string[80]; |
| |
| wpabuf_put_str(buf, wps_device_xml_prefix); |
| |
| /* |
| * Add required fields with default values if not configured. Add |
| * optional and recommended fields only if configured. |
| */ |
| s = iface->wps->friendly_name; |
| s = ((s && *s) ? s : "WPS Access Point"); |
| xml_add_tagged_data(buf, "friendlyName", s); |
| |
| s = iface->wps->dev.manufacturer; |
| s = ((s && *s) ? s : ""); |
| xml_add_tagged_data(buf, "manufacturer", s); |
| |
| if (iface->wps->manufacturer_url) |
| xml_add_tagged_data(buf, "manufacturerURL", |
| iface->wps->manufacturer_url); |
| |
| if (iface->wps->model_description) |
| xml_add_tagged_data(buf, "modelDescription", |
| iface->wps->model_description); |
| |
| s = iface->wps->dev.model_name; |
| s = ((s && *s) ? s : ""); |
| xml_add_tagged_data(buf, "modelName", s); |
| |
| if (iface->wps->dev.model_number) |
| xml_add_tagged_data(buf, "modelNumber", |
| iface->wps->dev.model_number); |
| |
| if (iface->wps->model_url) |
| xml_add_tagged_data(buf, "modelURL", iface->wps->model_url); |
| |
| if (iface->wps->dev.serial_number) |
| xml_add_tagged_data(buf, "serialNumber", |
| iface->wps->dev.serial_number); |
| |
| uuid_bin2str(iface->wps->uuid, uuid_string, sizeof(uuid_string)); |
| s = uuid_string; |
| /* Need "uuid:" prefix, thus we can't use xml_add_tagged_data() |
| * easily... |
| */ |
| wpabuf_put_str(buf, "<UDN>uuid:"); |
| xml_data_encode(buf, s, os_strlen(s)); |
| wpabuf_put_str(buf, "</UDN>\n"); |
| |
| if (iface->wps->upc) |
| xml_add_tagged_data(buf, "UPC", iface->wps->upc); |
| |
| wpabuf_put_str(buf, wps_device_xml_postfix); |
| } |
| |
| |
| static void http_put_reply_code(struct wpabuf *buf, enum http_reply_code code) |
| { |
| wpabuf_put_str(buf, "HTTP/1.1 "); |
| switch (code) { |
| case HTTP_OK: |
| wpabuf_put_str(buf, "200 OK\r\n"); |
| break; |
| case HTTP_BAD_REQUEST: |
| wpabuf_put_str(buf, "400 Bad request\r\n"); |
| break; |
| case HTTP_PRECONDITION_FAILED: |
| wpabuf_put_str(buf, "412 Precondition failed\r\n"); |
| break; |
| case HTTP_UNIMPLEMENTED: |
| wpabuf_put_str(buf, "501 Unimplemented\r\n"); |
| break; |
| case HTTP_INTERNAL_SERVER_ERROR: |
| default: |
| wpabuf_put_str(buf, "500 Internal server error\r\n"); |
| break; |
| } |
| } |
| |
| |
| static void http_put_date(struct wpabuf *buf) |
| { |
| wpabuf_put_str(buf, "Date: "); |
| format_date(buf); |
| wpabuf_put_str(buf, "\r\n"); |
| } |
| |
| |
| static void http_put_empty(struct wpabuf *buf, enum http_reply_code code) |
| { |
| http_put_reply_code(buf, code); |
| wpabuf_put_str(buf, http_server_hdr); |
| wpabuf_put_str(buf, http_connection_close); |
| wpabuf_put_str(buf, "Content-Length: 0\r\n" |
| "\r\n"); |
| } |
| |
| |
| /* Given that we have received a header w/ GET, act upon it |
| * |
| * Format of GET (case-insensitive): |
| * |
| * First line must be: |
| * GET /<file> HTTP/1.1 |
| * Since we don't do anything fancy we just ignore other lines. |
| * |
| * Our response (if no error) which includes only required lines is: |
| * HTTP/1.1 200 OK |
| * Connection: close |
| * Content-Type: text/xml |
| * Date: <rfc1123-date> |
| * |
| * Header lines must end with \r\n |
| * Per RFC 2616, content-length: is not required but connection:close |
| * would appear to be required (given that we will be closing it!). |
| */ |
| static void web_connection_parse_get(struct upnp_wps_device_sm *sm, |
| struct http_request *hreq, char *filename) |
| { |
| struct wpabuf *buf; /* output buffer, allocated */ |
| char *put_length_here; |
| char *body_start; |
| enum { |
| GET_DEVICE_XML_FILE, |
| GET_SCPD_XML_FILE |
| } req; |
| size_t extra_len = 0; |
| int body_length; |
| char len_buf[10]; |
| struct upnp_wps_device_interface *iface; |
| |
| iface = dl_list_first(&sm->interfaces, |
| struct upnp_wps_device_interface, list); |
| if (iface == NULL) { |
| http_request_deinit(hreq); |
| return; |
| } |
| |
| /* |
| * It is not required that filenames be case insensitive but it is |
| * allowed and cannot hurt here. |
| */ |
| if (os_strcasecmp(filename, UPNP_WPS_DEVICE_XML_FILE) == 0) { |
| wpa_printf(MSG_DEBUG, "WPS UPnP: HTTP GET for device XML"); |
| req = GET_DEVICE_XML_FILE; |
| extra_len = 3000; |
| if (iface->wps->friendly_name) |
| extra_len += os_strlen(iface->wps->friendly_name); |
| if (iface->wps->manufacturer_url) |
| extra_len += os_strlen(iface->wps->manufacturer_url); |
| if (iface->wps->model_description) |
| extra_len += os_strlen(iface->wps->model_description); |
| if (iface->wps->model_url) |
| extra_len += os_strlen(iface->wps->model_url); |
| if (iface->wps->upc) |
| extra_len += os_strlen(iface->wps->upc); |
| } else if (!os_strcasecmp(filename, UPNP_WPS_SCPD_XML_FILE)) { |
| wpa_printf(MSG_DEBUG, "WPS UPnP: HTTP GET for SCPD XML"); |
| req = GET_SCPD_XML_FILE; |
| extra_len = os_strlen(wps_scpd_xml); |
| } else { |
| /* File not found */ |
| wpa_printf(MSG_DEBUG, "WPS UPnP: HTTP GET file not found: %s", |
| filename); |
| buf = wpabuf_alloc(200); |
| if (buf == NULL) { |
| http_request_deinit(hreq); |
| return; |
| } |
| wpabuf_put_str(buf, |
| "HTTP/1.1 404 Not Found\r\n" |
| "Connection: close\r\n"); |
| |
| http_put_date(buf); |
| |
| /* terminating empty line */ |
| wpabuf_put_str(buf, "\r\n"); |
| |
| goto send_buf; |
| } |
| |
| buf = wpabuf_alloc(1000 + extra_len); |
| if (buf == NULL) { |
| http_request_deinit(hreq); |
| return; |
| } |
| |
| wpabuf_put_str(buf, |
| "HTTP/1.1 200 OK\r\n" |
| "Content-Type: text/xml; charset=\"utf-8\"\r\n"); |
| wpabuf_put_str(buf, "Server: Unspecified, UPnP/1.0, Unspecified\r\n"); |
| wpabuf_put_str(buf, "Connection: close\r\n"); |
| wpabuf_put_str(buf, "Content-Length: "); |
| /* |
| * We will paste the length in later, leaving some extra whitespace. |
| * HTTP code is supposed to be tolerant of extra whitespace. |
| */ |
| put_length_here = wpabuf_put(buf, 0); |
| wpabuf_put_str(buf, " \r\n"); |
| |
| http_put_date(buf); |
| |
| /* terminating empty line */ |
| wpabuf_put_str(buf, "\r\n"); |
| |
| body_start = wpabuf_put(buf, 0); |
| |
| switch (req) { |
| case GET_DEVICE_XML_FILE: |
| format_wps_device_xml(iface, sm, buf); |
| break; |
| case GET_SCPD_XML_FILE: |
| wpabuf_put_str(buf, wps_scpd_xml); |
| break; |
| } |
| |
| /* Now patch in the content length at the end */ |
| body_length = (char *) wpabuf_put(buf, 0) - body_start; |
| os_snprintf(len_buf, 10, "%d", body_length); |
| os_memcpy(put_length_here, len_buf, os_strlen(len_buf)); |
| |
| send_buf: |
| http_request_send_and_deinit(hreq, buf); |
| } |
| |
| |
| static enum http_reply_code |
| web_process_get_device_info(struct upnp_wps_device_sm *sm, |
| struct wpabuf **reply, const char **replyname) |
| { |
| static const char *name = "NewDeviceInfo"; |
| struct wps_config cfg; |
| struct upnp_wps_device_interface *iface; |
| struct upnp_wps_peer *peer; |
| |
| iface = dl_list_first(&sm->interfaces, |
| struct upnp_wps_device_interface, list); |
| |
| wpa_printf(MSG_DEBUG, "WPS UPnP: GetDeviceInfo"); |
| |
| if (!iface || iface->ctx->ap_pin == NULL) |
| return HTTP_INTERNAL_SERVER_ERROR; |
| |
| peer = &iface->peer; |
| |
| /* |
| * Request for DeviceInfo, i.e., M1 TLVs. This is a start of WPS |
| * registration over UPnP with the AP acting as an Enrollee. It should |
| * be noted that this is frequently used just to get the device data, |
| * i.e., there may not be any intent to actually complete the |
| * registration. |
| */ |
| |
| if (peer->wps) |
| wps_deinit(peer->wps); |
| |
| os_memset(&cfg, 0, sizeof(cfg)); |
| cfg.wps = iface->wps; |
| cfg.pin = (u8 *) iface->ctx->ap_pin; |
| cfg.pin_len = os_strlen(iface->ctx->ap_pin); |
| peer->wps = wps_init(&cfg); |
| if (peer->wps) { |
| enum wsc_op_code op_code; |
| *reply = wps_get_msg(peer->wps, &op_code); |
| if (*reply == NULL) { |
| wps_deinit(peer->wps); |
| peer->wps = NULL; |
| } |
| } else |
| *reply = NULL; |
| if (*reply == NULL) { |
| wpa_printf(MSG_INFO, "WPS UPnP: Failed to get DeviceInfo"); |
| return HTTP_INTERNAL_SERVER_ERROR; |
| } |
| *replyname = name; |
| return HTTP_OK; |
| } |
| |
| |
| static enum http_reply_code |
| web_process_put_message(struct upnp_wps_device_sm *sm, char *data, |
| struct wpabuf **reply, const char **replyname) |
| { |
| struct wpabuf *msg; |
| static const char *name = "NewOutMessage"; |
| enum http_reply_code ret; |
| enum wps_process_res res; |
| enum wsc_op_code op_code; |
| struct upnp_wps_device_interface *iface; |
| |
| iface = dl_list_first(&sm->interfaces, |
| struct upnp_wps_device_interface, list); |
| if (!iface) |
| return HTTP_INTERNAL_SERVER_ERROR; |
| |
| /* |
| * PutMessage is used by external UPnP-based Registrar to perform WPS |
| * operation with the access point itself; as compared with |
| * PutWLANResponse which is for proxying. |
| */ |
| wpa_printf(MSG_DEBUG, "WPS UPnP: PutMessage"); |
| msg = xml_get_base64_item(data, "NewInMessage", &ret); |
| if (msg == NULL) |
| return ret; |
| res = wps_process_msg(iface->peer.wps, WSC_UPnP, msg); |
| if (res == WPS_FAILURE) |
| *reply = NULL; |
| else |
| *reply = wps_get_msg(iface->peer.wps, &op_code); |
| wpabuf_free(msg); |
| if (*reply == NULL) |
| return HTTP_INTERNAL_SERVER_ERROR; |
| *replyname = name; |
| return HTTP_OK; |
| } |
| |
| |
| static enum http_reply_code |
| web_process_put_wlan_response(struct upnp_wps_device_sm *sm, char *data, |
| struct wpabuf **reply, const char **replyname) |
| { |
| struct wpabuf *msg; |
| enum http_reply_code ret; |
| u8 macaddr[ETH_ALEN]; |
| int ev_type; |
| int type; |
| char *val; |
| struct upnp_wps_device_interface *iface; |
| int ok = 0; |
| |
| /* |
| * External UPnP-based Registrar is passing us a message to be proxied |
| * over to a Wi-Fi -based client of ours. |
| */ |
| |
| wpa_printf(MSG_DEBUG, "WPS UPnP: PutWLANResponse"); |
| msg = xml_get_base64_item(data, "NewMessage", &ret); |
| if (msg == NULL) { |
| wpa_printf(MSG_DEBUG, "WPS UPnP: Could not extract NewMessage " |
| "from PutWLANResponse"); |
| return ret; |
| } |
| val = xml_get_first_item(data, "NewWLANEventType"); |
| if (val == NULL) { |
| wpa_printf(MSG_DEBUG, "WPS UPnP: No NewWLANEventType in " |
| "PutWLANResponse"); |
| wpabuf_free(msg); |
| return UPNP_ARG_VALUE_INVALID; |
| } |
| ev_type = atol(val); |
| os_free(val); |
| val = xml_get_first_item(data, "NewWLANEventMAC"); |
| if (val == NULL) { |
| wpa_printf(MSG_DEBUG, "WPS UPnP: No NewWLANEventMAC in " |
| "PutWLANResponse"); |
| wpabuf_free(msg); |
| return UPNP_ARG_VALUE_INVALID; |
| } |
| if (hwaddr_aton(val, macaddr)) { |
| wpa_printf(MSG_DEBUG, "WPS UPnP: Invalid NewWLANEventMAC in " |
| "PutWLANResponse: '%s'", val); |
| #ifdef CONFIG_WPS_STRICT |
| { |
| struct wps_parse_attr attr; |
| if (wps_parse_msg(msg, &attr) < 0 || attr.version2) { |
| wpabuf_free(msg); |
| os_free(val); |
| return UPNP_ARG_VALUE_INVALID; |
| } |
| } |
| #endif /* CONFIG_WPS_STRICT */ |
| if (hwaddr_aton2(val, macaddr) > 0) { |
| /* |
| * At least some versions of Intel PROset seem to be |
| * using dot-deliminated MAC address format here. |
| */ |
| wpa_printf(MSG_DEBUG, "WPS UPnP: Workaround - allow " |
| "incorrect MAC address format in " |
| "NewWLANEventMAC: %s -> " MACSTR, |
| val, MAC2STR(macaddr)); |
| } else { |
| wpabuf_free(msg); |
| os_free(val); |
| return UPNP_ARG_VALUE_INVALID; |
| } |
| } |
| os_free(val); |
| if (ev_type == UPNP_WPS_WLANEVENT_TYPE_EAP) { |
| struct wps_parse_attr attr; |
| if (wps_parse_msg(msg, &attr) < 0 || |
| attr.msg_type == NULL) |
| type = -1; |
| else |
| type = *attr.msg_type; |
| wpa_printf(MSG_DEBUG, "WPS UPnP: Message Type %d", type); |
| } else |
| type = -1; |
| dl_list_for_each(iface, &sm->interfaces, |
| struct upnp_wps_device_interface, list) { |
| if (iface->ctx->rx_req_put_wlan_response && |
| iface->ctx->rx_req_put_wlan_response(iface->priv, ev_type, |
| macaddr, msg, type) |
| == 0) |
| ok = 1; |
| } |
| |
| if (!ok) { |
| wpa_printf(MSG_INFO, "WPS UPnP: Fail: sm->ctx->" |
| "rx_req_put_wlan_response"); |
| wpabuf_free(msg); |
| return HTTP_INTERNAL_SERVER_ERROR; |
| } |
| wpabuf_free(msg); |
| *replyname = NULL; |
| *reply = NULL; |
| return HTTP_OK; |
| } |
| |
| |
| static int find_er_addr(struct subscription *s, struct sockaddr_in *cli) |
| { |
| struct subscr_addr *a; |
| |
| dl_list_for_each(a, &s->addr_list, struct subscr_addr, list) { |
| if (cli->sin_addr.s_addr == a->saddr.sin_addr.s_addr) |
| return 1; |
| } |
| return 0; |
| } |
| |
| |
| static struct subscription * find_er(struct upnp_wps_device_sm *sm, |
| struct sockaddr_in *cli) |
| { |
| struct subscription *s; |
| dl_list_for_each(s, &sm->subscriptions, struct subscription, list) |
| if (find_er_addr(s, cli)) |
| return s; |
| return NULL; |
| } |
| |
| |
| static enum http_reply_code |
| web_process_set_selected_registrar(struct upnp_wps_device_sm *sm, |
| struct sockaddr_in *cli, char *data, |
| struct wpabuf **reply, |
| const char **replyname) |
| { |
| struct wpabuf *msg; |
| enum http_reply_code ret; |
| struct subscription *s; |
| struct upnp_wps_device_interface *iface; |
| int err = 0; |
| |
| wpa_printf(MSG_DEBUG, "WPS UPnP: SetSelectedRegistrar"); |
| s = find_er(sm, cli); |
| if (s == NULL) { |
| wpa_printf(MSG_DEBUG, "WPS UPnP: Ignore SetSelectedRegistrar " |
| "from unknown ER"); |
| return UPNP_ACTION_FAILED; |
| } |
| msg = xml_get_base64_item(data, "NewMessage", &ret); |
| if (msg == NULL) |
| return ret; |
| dl_list_for_each(iface, &sm->interfaces, |
| struct upnp_wps_device_interface, list) { |
| if (upnp_er_set_selected_registrar(iface->wps->registrar, s, |
| msg)) |
| err = 1; |
| } |
| wpabuf_free(msg); |
| if (err) |
| return HTTP_INTERNAL_SERVER_ERROR; |
| *replyname = NULL; |
| *reply = NULL; |
| return HTTP_OK; |
| } |
| |
| |
| static const char *soap_prefix = |
| "<?xml version=\"1.0\"?>\n" |
| "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" " |
| "s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">\n" |
| "<s:Body>\n"; |
| static const char *soap_postfix = |
| "</s:Body>\n</s:Envelope>\n"; |
| |
| static const char *soap_error_prefix = |
| "<s:Fault>\n" |
| "<faultcode>s:Client</faultcode>\n" |
| "<faultstring>UPnPError</faultstring>\n" |
| "<detail>\n" |
| "<UPnPError xmlns=\"urn:schemas-upnp-org:control-1-0\">\n"; |
| static const char *soap_error_postfix = |
| "<errorDescription>Error</errorDescription>\n" |
| "</UPnPError>\n" |
| "</detail>\n" |
| "</s:Fault>\n"; |
| |
| static void web_connection_send_reply(struct http_request *req, |
| enum http_reply_code ret, |
| const char *action, int action_len, |
| const struct wpabuf *reply, |
| const char *replyname) |
| { |
| struct wpabuf *buf; |
| char *replydata; |
| char *put_length_here = NULL; |
| char *body_start = NULL; |
| |
| if (reply) { |
| size_t len; |
| replydata = (char *) base64_encode(wpabuf_head(reply), |
| wpabuf_len(reply), &len); |
| } else |
| replydata = NULL; |
| |
| /* Parameters of the response: |
| * action(action_len) -- action we are responding to |
| * replyname -- a name we need for the reply |
| * replydata -- NULL or null-terminated string |
| */ |
| buf = wpabuf_alloc(1000 + (replydata ? os_strlen(replydata) : 0U) + |
| (action_len > 0 ? action_len * 2 : 0)); |
| if (buf == NULL) { |
| wpa_printf(MSG_INFO, "WPS UPnP: Cannot allocate reply to " |
| "POST"); |
| os_free(replydata); |
| http_request_deinit(req); |
| return; |
| } |
| |
| /* |
| * Assuming we will be successful, put in the output header first. |
| * Note: we do not keep connections alive (and httpread does |
| * not support it)... therefore we must have Connection: close. |
| */ |
| if (ret == HTTP_OK) { |
| wpabuf_put_str(buf, |
| "HTTP/1.1 200 OK\r\n" |
| "Content-Type: text/xml; " |
| "charset=\"utf-8\"\r\n"); |
| } else { |
| wpabuf_printf(buf, "HTTP/1.1 %d Error\r\n", ret); |
| } |
| wpabuf_put_str(buf, http_connection_close); |
| |
| wpabuf_put_str(buf, "Content-Length: "); |
| /* |
| * We will paste the length in later, leaving some extra whitespace. |
| * HTTP code is supposed to be tolerant of extra whitespace. |
| */ |
| put_length_here = wpabuf_put(buf, 0); |
| wpabuf_put_str(buf, " \r\n"); |
| |
| http_put_date(buf); |
| |
| /* terminating empty line */ |
| wpabuf_put_str(buf, "\r\n"); |
| |
| body_start = wpabuf_put(buf, 0); |
| |
| if (ret == HTTP_OK) { |
| wpabuf_put_str(buf, soap_prefix); |
| wpabuf_put_str(buf, "<u:"); |
| wpabuf_put_data(buf, action, action_len); |
| wpabuf_put_str(buf, "Response xmlns:u=\""); |
| wpabuf_put_str(buf, urn_wfawlanconfig); |
| wpabuf_put_str(buf, "\">\n"); |
| if (replydata && replyname) { |
| /* TODO: might possibly need to escape part of reply |
| * data? ... |
| * probably not, unlikely to have ampersand(&) or left |
| * angle bracket (<) in it... |
| */ |
| wpabuf_printf(buf, "<%s>", replyname); |
| wpabuf_put_str(buf, replydata); |
| wpabuf_printf(buf, "</%s>\n", replyname); |
| } |
| wpabuf_put_str(buf, "</u:"); |
| wpabuf_put_data(buf, action, action_len); |
| wpabuf_put_str(buf, "Response>\n"); |
| wpabuf_put_str(buf, soap_postfix); |
| } else { |
| /* Error case */ |
| wpabuf_put_str(buf, soap_prefix); |
| wpabuf_put_str(buf, soap_error_prefix); |
| wpabuf_printf(buf, "<errorCode>%d</errorCode>\n", ret); |
| wpabuf_put_str(buf, soap_error_postfix); |
| wpabuf_put_str(buf, soap_postfix); |
| } |
| os_free(replydata); |
| |
| /* Now patch in the content length at the end */ |
| if (body_start && put_length_here) { |
| int body_length = (char *) wpabuf_put(buf, 0) - body_start; |
| char len_buf[10]; |
| os_snprintf(len_buf, sizeof(len_buf), "%d", body_length); |
| os_memcpy(put_length_here, len_buf, os_strlen(len_buf)); |
| } |
| |
| http_request_send_and_deinit(req, buf); |
| } |
| |
| |
| static const char * web_get_action(struct http_request *req, |
| size_t *action_len) |
| { |
| const char *match; |
| int match_len; |
| char *b; |
| char *action; |
| |
| *action_len = 0; |
| /* The SOAPAction line of the header tells us what we want to do */ |
| b = http_request_get_hdr_line(req, "SOAPAction:"); |
| if (b == NULL) |
| return NULL; |
| if (*b == '"') |
| b++; |
| else |
| return NULL; |
| match = urn_wfawlanconfig; |
| match_len = os_strlen(urn_wfawlanconfig) - 1; |
| if (os_strncasecmp(b, match, match_len)) |
| return NULL; |
| b += match_len; |
| /* skip over version */ |
| while (isgraph(*b) && *b != '#') |
| b++; |
| if (*b != '#') |
| return NULL; |
| b++; |
| /* Following the sharp(#) should be the action and a double quote */ |
| action = b; |
| while (isgraph(*b) && *b != '"') |
| b++; |
| if (*b != '"') |
| return NULL; |
| *action_len = b - action; |
| return action; |
| } |
| |
| |
| /* Given that we have received a header w/ POST, act upon it |
| * |
| * Format of POST (case-insensitive): |
| * |
| * First line must be: |
| * POST /<file> HTTP/1.1 |
| * Since we don't do anything fancy we just ignore other lines. |
| * |
| * Our response (if no error) which includes only required lines is: |
| * HTTP/1.1 200 OK |
| * Connection: close |
| * Content-Type: text/xml |
| * Date: <rfc1123-date> |
| * |
| * Header lines must end with \r\n |
| * Per RFC 2616, content-length: is not required but connection:close |
| * would appear to be required (given that we will be closing it!). |
| */ |
| static void web_connection_parse_post(struct upnp_wps_device_sm *sm, |
| struct sockaddr_in *cli, |
| struct http_request *req, |
| const char *filename) |
| { |
| enum http_reply_code ret; |
| char *data = http_request_get_data(req); /* body of http msg */ |
| const char *action = NULL; |
| size_t action_len = 0; |
| const char *replyname = NULL; /* argument name for the reply */ |
| struct wpabuf *reply = NULL; /* data for the reply */ |
| |
| if (os_strcasecmp(filename, UPNP_WPS_DEVICE_CONTROL_FILE)) { |
| wpa_printf(MSG_INFO, "WPS UPnP: Invalid POST filename %s", |
| filename); |
| ret = HTTP_NOT_FOUND; |
| goto bad; |
| } |
| |
| ret = UPNP_INVALID_ACTION; |
| action = web_get_action(req, &action_len); |
| if (action == NULL) |
| goto bad; |
| |
| if (!os_strncasecmp("GetDeviceInfo", action, action_len)) |
| ret = web_process_get_device_info(sm, &reply, &replyname); |
| else if (!os_strncasecmp("PutMessage", action, action_len)) |
| ret = web_process_put_message(sm, data, &reply, &replyname); |
| else if (!os_strncasecmp("PutWLANResponse", action, action_len)) |
| ret = web_process_put_wlan_response(sm, data, &reply, |
| &replyname); |
| else if (!os_strncasecmp("SetSelectedRegistrar", action, action_len)) |
| ret = web_process_set_selected_registrar(sm, cli, data, &reply, |
| &replyname); |
| else |
| wpa_printf(MSG_INFO, "WPS UPnP: Unknown POST type"); |
| |
| bad: |
| if (ret != HTTP_OK) |
| wpa_printf(MSG_INFO, "WPS UPnP: POST failure ret=%d", ret); |
| web_connection_send_reply(req, ret, action, action_len, reply, |
| replyname); |
| wpabuf_free(reply); |
| } |
| |
| |
| /* Given that we have received a header w/ SUBSCRIBE, act upon it |
| * |
| * Format of SUBSCRIBE (case-insensitive): |
| * |
| * First line must be: |
| * SUBSCRIBE /wps_event HTTP/1.1 |
| * |
| * Our response (if no error) which includes only required lines is: |
| * HTTP/1.1 200 OK |
| * Server: xx, UPnP/1.0, xx |
| * SID: uuid:xxxxxxxxx |
| * Timeout: Second-<n> |
| * Content-Length: 0 |
| * Date: xxxx |
| * |
| * Header lines must end with \r\n |
| * Per RFC 2616, content-length: is not required but connection:close |
| * would appear to be required (given that we will be closing it!). |
| */ |
| static void web_connection_parse_subscribe(struct upnp_wps_device_sm *sm, |
| struct http_request *req, |
| const char *filename) |
| { |
| struct wpabuf *buf; |
| char *b; |
| char *hdr = http_request_get_hdr(req); |
| char *h; |
| char *match; |
| int match_len; |
| char *end; |
| int len; |
| int got_nt = 0; |
| u8 uuid[UUID_LEN]; |
| int got_uuid = 0; |
| char *callback_urls = NULL; |
| struct subscription *s = NULL; |
| enum http_reply_code ret = HTTP_INTERNAL_SERVER_ERROR; |
| |
| buf = wpabuf_alloc(1000); |
| if (buf == NULL) { |
| http_request_deinit(req); |
| return; |
| } |
| |
| wpa_hexdump_ascii(MSG_DEBUG, "WPS UPnP: HTTP SUBSCRIBE", |
| (u8 *) hdr, os_strlen(hdr)); |
| |
| /* Parse/validate headers */ |
| h = hdr; |
| /* First line: SUBSCRIBE /wps_event HTTP/1.1 |
| * has already been parsed. |
| */ |
| if (os_strcasecmp(filename, UPNP_WPS_DEVICE_EVENT_FILE) != 0) { |
| ret = HTTP_PRECONDITION_FAILED; |
| goto error; |
| } |
| wpa_printf(MSG_DEBUG, "WPS UPnP: HTTP SUBSCRIBE for event"); |
| end = os_strchr(h, '\n'); |
| |
| while (end) { |
| /* Option line by option line */ |
| h = end + 1; |
| end = os_strchr(h, '\n'); |
| if (end == NULL) |
| break; /* no unterminated lines allowed */ |
| |
| /* NT assures that it is our type of subscription; |
| * not used for a renewal. |
| **/ |
| match = "NT:"; |
| match_len = os_strlen(match); |
| if (os_strncasecmp(h, match, match_len) == 0) { |
| h += match_len; |
| while (*h == ' ' || *h == '\t') |
| h++; |
| match = "upnp:event"; |
| match_len = os_strlen(match); |
| if (os_strncasecmp(h, match, match_len) != 0) { |
| ret = HTTP_BAD_REQUEST; |
| goto error; |
| } |
| got_nt = 1; |
| continue; |
| } |
| /* HOST should refer to us */ |
| #if 0 |
| match = "HOST:"; |
| match_len = os_strlen(match); |
| if (os_strncasecmp(h, match, match_len) == 0) { |
| h += match_len; |
| while (*h == ' ' || *h == '\t') |
| h++; |
| ..... |
| } |
| #endif |
| /* CALLBACK gives one or more URLs for NOTIFYs |
| * to be sent as a result of the subscription. |
| * Each URL is enclosed in angle brackets. |
| */ |
| match = "CALLBACK:"; |
| match_len = os_strlen(match); |
| if (os_strncasecmp(h, match, match_len) == 0) { |
| h += match_len; |
| while (*h == ' ' || *h == '\t') |
| h++; |
| len = end - h; |
| os_free(callback_urls); |
| callback_urls = dup_binstr(h, len); |
| if (callback_urls == NULL) { |
| ret = HTTP_INTERNAL_SERVER_ERROR; |
| goto error; |
| } |
| continue; |
| } |
| /* SID is only for renewal */ |
| match = "SID:"; |
| match_len = os_strlen(match); |
| if (os_strncasecmp(h, match, match_len) == 0) { |
| h += match_len; |
| while (*h == ' ' || *h == '\t') |
| h++; |
| match = "uuid:"; |
| match_len = os_strlen(match); |
| if (os_strncasecmp(h, match, match_len) != 0) { |
| ret = HTTP_BAD_REQUEST; |
| goto error; |
| } |
| h += match_len; |
| while (*h == ' ' || *h == '\t') |
| h++; |
| if (uuid_str2bin(h, uuid)) { |
| ret = HTTP_BAD_REQUEST; |
| goto error; |
| } |
| got_uuid = 1; |
| continue; |
| } |
| /* TIMEOUT is requested timeout, but apparently we can |
| * just ignore this. |
| */ |
| } |
| |
| if (got_uuid) { |
| /* renewal */ |
| wpa_printf(MSG_DEBUG, "WPS UPnP: Subscription renewal"); |
| if (callback_urls) { |
| ret = HTTP_BAD_REQUEST; |
| goto error; |
| } |
| s = subscription_renew(sm, uuid); |
| if (s == NULL) { |
| char str[80]; |
| uuid_bin2str(uuid, str, sizeof(str)); |
| wpa_printf(MSG_DEBUG, "WPS UPnP: Could not find " |
| "SID %s", str); |
| ret = HTTP_PRECONDITION_FAILED; |
| goto error; |
| } |
| } else if (callback_urls) { |
| wpa_printf(MSG_DEBUG, "WPS UPnP: New subscription"); |
| if (!got_nt) { |
| ret = HTTP_PRECONDITION_FAILED; |
| goto error; |
| } |
| s = subscription_start(sm, callback_urls); |
| if (s == NULL) { |
| ret = HTTP_INTERNAL_SERVER_ERROR; |
| goto error; |
| } |
| } else { |
| ret = HTTP_PRECONDITION_FAILED; |
| goto error; |
| } |
| |
| /* success */ |
| http_put_reply_code(buf, HTTP_OK); |
| wpabuf_put_str(buf, http_server_hdr); |
| wpabuf_put_str(buf, http_connection_close); |
| wpabuf_put_str(buf, "Content-Length: 0\r\n"); |
| wpabuf_put_str(buf, "SID: uuid:"); |
| /* subscription id */ |
| b = wpabuf_put(buf, 0); |
| uuid_bin2str(s->uuid, b, 80); |
| wpa_printf(MSG_DEBUG, "WPS UPnP: Assigned SID %s", b); |
| wpabuf_put(buf, os_strlen(b)); |
| wpabuf_put_str(buf, "\r\n"); |
| wpabuf_printf(buf, "Timeout: Second-%d\r\n", UPNP_SUBSCRIBE_SEC); |
| http_put_date(buf); |
| /* And empty line to terminate header: */ |
| wpabuf_put_str(buf, "\r\n"); |
| |
| os_free(callback_urls); |
| http_request_send_and_deinit(req, buf); |
| return; |
| |
| error: |
| /* Per UPnP spec: |
| * Errors |
| * Incompatible headers |
| * 400 Bad Request. If SID header and one of NT or CALLBACK headers |
| * are present, the publisher must respond with HTTP error |
| * 400 Bad Request. |
| * Missing or invalid CALLBACK |
| * 412 Precondition Failed. If CALLBACK header is missing or does not |
| * contain a valid HTTP URL, the publisher must respond with HTTP |
| * error 412 Precondition Failed. |
| * Invalid NT |
| * 412 Precondition Failed. If NT header does not equal upnp:event, |
| * the publisher must respond with HTTP error 412 Precondition |
| * Failed. |
| * [For resubscription, use 412 if unknown uuid]. |
| * Unable to accept subscription |
| * 5xx. If a publisher is not able to accept a subscription (such as |
| * due to insufficient resources), it must respond with a |
| * HTTP 500-series error code. |
| * 599 Too many subscriptions (not a standard HTTP error) |
| */ |
| wpa_printf(MSG_DEBUG, "WPS UPnP: SUBSCRIBE failed - return %d", ret); |
| http_put_empty(buf, ret); |
| http_request_send_and_deinit(req, buf); |
| os_free(callback_urls); |
| } |
| |
| |
| /* Given that we have received a header w/ UNSUBSCRIBE, act upon it |
| * |
| * Format of UNSUBSCRIBE (case-insensitive): |
| * |
| * First line must be: |
| * UNSUBSCRIBE /wps_event HTTP/1.1 |
| * |
| * Our response (if no error) which includes only required lines is: |
| * HTTP/1.1 200 OK |
| * Content-Length: 0 |
| * |
| * Header lines must end with \r\n |
| * Per RFC 2616, content-length: is not required but connection:close |
| * would appear to be required (given that we will be closing it!). |
| */ |
| static void web_connection_parse_unsubscribe(struct upnp_wps_device_sm *sm, |
| struct http_request *req, |
| const char *filename) |
| { |
| struct wpabuf *buf; |
| char *hdr = http_request_get_hdr(req); |
| char *h; |
| char *match; |
| int match_len; |
| char *end; |
| u8 uuid[UUID_LEN]; |
| int got_uuid = 0; |
| struct subscription *s = NULL; |
| enum http_reply_code ret = HTTP_INTERNAL_SERVER_ERROR; |
| |
| /* Parse/validate headers */ |
| h = hdr; |
| /* First line: UNSUBSCRIBE /wps_event HTTP/1.1 |
| * has already been parsed. |
| */ |
| if (os_strcasecmp(filename, UPNP_WPS_DEVICE_EVENT_FILE) != 0) { |
| ret = HTTP_PRECONDITION_FAILED; |
| goto send_msg; |
| } |
| wpa_printf(MSG_DEBUG, "WPS UPnP: HTTP UNSUBSCRIBE for event"); |
| end = os_strchr(h, '\n'); |
| |
| while (end) { |
| /* Option line by option line */ |
| h = end + 1; |
| end = os_strchr(h, '\n'); |
| if (end == NULL) |
| break; /* no unterminated lines allowed */ |
| |
| /* HOST should refer to us */ |
| #if 0 |
| match = "HOST:"; |
| match_len = os_strlen(match); |
| if (os_strncasecmp(h, match, match_len) == 0) { |
| h += match_len; |
| while (*h == ' ' || *h == '\t') |
| h++; |
| ..... |
| } |
| #endif |
| match = "SID:"; |
| match_len = os_strlen(match); |
| if (os_strncasecmp(h, match, match_len) == 0) { |
| h += match_len; |
| while (*h == ' ' || *h == '\t') |
| h++; |
| match = "uuid:"; |
| match_len = os_strlen(match); |
| if (os_strncasecmp(h, match, match_len) != 0) { |
| ret = HTTP_BAD_REQUEST; |
| goto send_msg; |
| } |
| h += match_len; |
| while (*h == ' ' || *h == '\t') |
| h++; |
| if (uuid_str2bin(h, uuid)) { |
| ret = HTTP_BAD_REQUEST; |
| goto send_msg; |
| } |
| got_uuid = 1; |
| continue; |
| } |
| |
| match = "NT:"; |
| match_len = os_strlen(match); |
| if (os_strncasecmp(h, match, match_len) == 0) { |
| ret = HTTP_BAD_REQUEST; |
| goto send_msg; |
| } |
| |
| match = "CALLBACK:"; |
| match_len = os_strlen(match); |
| if (os_strncasecmp(h, match, match_len) == 0) { |
| ret = HTTP_BAD_REQUEST; |
| goto send_msg; |
| } |
| } |
| |
| if (got_uuid) { |
| s = subscription_find(sm, uuid); |
| if (s) { |
| struct subscr_addr *sa; |
| sa = dl_list_first(&s->addr_list, struct subscr_addr, |
| list); |
| wpa_printf(MSG_DEBUG, "WPS UPnP: Unsubscribing %p %s", |
| s, (sa && sa->domain_and_port) ? |
| sa->domain_and_port : "-null-"); |
| dl_list_del(&s->list); |
| subscription_destroy(s); |
| } else { |
| wpa_printf(MSG_INFO, "WPS UPnP: Could not find matching subscription to unsubscribe"); |
| ret = HTTP_PRECONDITION_FAILED; |
| goto send_msg; |
| } |
| } else { |
| wpa_printf(MSG_INFO, "WPS UPnP: Unsubscribe fails (not " |
| "found)"); |
| ret = HTTP_PRECONDITION_FAILED; |
| goto send_msg; |
| } |
| |
| ret = HTTP_OK; |
| |
| send_msg: |
| buf = wpabuf_alloc(200); |
| if (buf == NULL) { |
| http_request_deinit(req); |
| return; |
| } |
| http_put_empty(buf, ret); |
| http_request_send_and_deinit(req, buf); |
| } |
| |
| |
| /* Send error in response to unknown requests */ |
| static void web_connection_unimplemented(struct http_request *req) |
| { |
| struct wpabuf *buf; |
| buf = wpabuf_alloc(200); |
| if (buf == NULL) { |
| http_request_deinit(req); |
| return; |
| } |
| http_put_empty(buf, HTTP_UNIMPLEMENTED); |
| http_request_send_and_deinit(req, buf); |
| } |
| |
| |
| |
| /* Called when we have gotten an apparently valid http request. |
| */ |
| static void web_connection_check_data(void *ctx, struct http_request *req) |
| { |
| struct upnp_wps_device_sm *sm = ctx; |
| enum httpread_hdr_type htype = http_request_get_type(req); |
| char *filename = http_request_get_uri(req); |
| struct sockaddr_in *cli = http_request_get_cli_addr(req); |
| |
| if (!filename) { |
| wpa_printf(MSG_INFO, "WPS UPnP: Could not get HTTP URI"); |
| http_request_deinit(req); |
| return; |
| } |
| /* Trim leading slashes from filename */ |
| while (*filename == '/') |
| filename++; |
| |
| wpa_printf(MSG_DEBUG, "WPS UPnP: Got HTTP request type %d from %s:%d", |
| htype, inet_ntoa(cli->sin_addr), htons(cli->sin_port)); |
| |
| switch (htype) { |
| case HTTPREAD_HDR_TYPE_GET: |
| web_connection_parse_get(sm, req, filename); |
| break; |
| case HTTPREAD_HDR_TYPE_POST: |
| web_connection_parse_post(sm, cli, req, filename); |
| break; |
| case HTTPREAD_HDR_TYPE_SUBSCRIBE: |
| web_connection_parse_subscribe(sm, req, filename); |
| break; |
| case HTTPREAD_HDR_TYPE_UNSUBSCRIBE: |
| web_connection_parse_unsubscribe(sm, req, filename); |
| break; |
| |
| /* We are not required to support M-POST; just plain |
| * POST is supposed to work, so we only support that. |
| * If for some reason we need to support M-POST, it is |
| * mostly the same as POST, with small differences. |
| */ |
| default: |
| /* Send 501 for anything else */ |
| web_connection_unimplemented(req); |
| break; |
| } |
| } |
| |
| |
| /* |
| * Listening for web connections |
| * We have a single TCP listening port, and hand off connections as we get |
| * them. |
| */ |
| |
| void web_listener_stop(struct upnp_wps_device_sm *sm) |
| { |
| http_server_deinit(sm->web_srv); |
| sm->web_srv = NULL; |
| } |
| |
| |
| int web_listener_start(struct upnp_wps_device_sm *sm) |
| { |
| struct in_addr addr; |
| addr.s_addr = sm->ip_addr; |
| sm->web_srv = http_server_init(&addr, -1, web_connection_check_data, |
| sm); |
| if (sm->web_srv == NULL) { |
| web_listener_stop(sm); |
| return -1; |
| } |
| sm->web_port = http_server_get_port(sm->web_srv); |
| |
| return 0; |
| } |