diff --git a/hostapd/ChangeLog b/hostapd/ChangeLog index 31b016520..d48bd151e 100644 --- a/hostapd/ChangeLog +++ b/hostapd/ChangeLog @@ -4,6 +4,7 @@ ChangeLog for hostapd * increased hostapd_cli ping interval to 5 seconds and made this configurable with a new command line options (-G) * driver_nl80211: use Linux socket filter to improve performance + * added support for external Registrars with WPS (UPnP transport) 2009-01-06 - v0.6.7 * added support for Wi-Fi Protected Setup (WPS) diff --git a/hostapd/Makefile b/hostapd/Makefile index 7d65f2e8b..3c6bd4a6d 100644 --- a/hostapd/Makefile +++ b/hostapd/Makefile @@ -311,6 +311,16 @@ NEED_DH_GROUPS=y NEED_SHA256=y NEED_CRYPTO=y NEED_BASE64=y + +ifdef CONFIG_WPS_UPNP +CFLAGS += -DCONFIG_WPS_UPNP +OBJS += ../src/wps/wps_upnp.o +OBJS += ../src/wps/wps_upnp_ssdp.o +OBJS += ../src/wps/wps_upnp_web.o +OBJS += ../src/wps/wps_upnp_event.o +OBJS += ../src/wps/httpread.o +endif + endif ifdef CONFIG_EAP_IKEV2 diff --git a/hostapd/config.c b/hostapd/config.c index 50faf03ab..41cc75cdc 100644 --- a/hostapd/config.c +++ b/hostapd/config.c @@ -2234,6 +2234,23 @@ struct hostapd_config * hostapd_config_read(const char *fname) line, pos); errors++; } + } else if (os_strcmp(buf, "upnp_iface") == 0) { + bss->upnp_iface = os_strdup(pos); + } else if (os_strcmp(buf, "friendly_name") == 0) { + os_free(bss->friendly_name); + bss->friendly_name = os_strdup(pos); + } else if (os_strcmp(buf, "manufacturer_url") == 0) { + os_free(bss->manufacturer_url); + bss->manufacturer_url = os_strdup(pos); + } else if (os_strcmp(buf, "model_description") == 0) { + os_free(bss->model_description); + bss->model_description = os_strdup(pos); + } else if (os_strcmp(buf, "model_url") == 0) { + os_free(bss->model_url); + bss->model_url = os_strdup(pos); + } else if (os_strcmp(buf, "upc") == 0) { + os_free(bss->upc); + bss->upc = os_strdup(pos); #endif /* CONFIG_WPS */ } else { wpa_printf(MSG_ERROR, "Line %d: unknown configuration " @@ -2441,6 +2458,12 @@ static void hostapd_config_free_bss(struct hostapd_bss_config *conf) os_free(conf->ap_pin); os_free(conf->extra_cred); os_free(conf->ap_settings); + os_free(conf->upnp_iface); + os_free(conf->friendly_name); + os_free(conf->manufacturer_url); + os_free(conf->model_description); + os_free(conf->model_url); + os_free(conf->upc); #endif /* CONFIG_WPS */ } diff --git a/hostapd/config.h b/hostapd/config.h index 87be50624..315968285 100644 --- a/hostapd/config.h +++ b/hostapd/config.h @@ -306,6 +306,12 @@ struct hostapd_bss_config { int wps_cred_processing; u8 *ap_settings; size_t ap_settings_len; + char *upnp_iface; + char *friendly_name; + char *manufacturer_url; + char *model_description; + char *model_url; + char *upc; #endif /* CONFIG_WPS */ }; diff --git a/hostapd/hostapd.conf b/hostapd/hostapd.conf index 9f9d7e3a3..a4a033a98 100644 --- a/hostapd/hostapd.conf +++ b/hostapd/hostapd.conf @@ -971,6 +971,28 @@ own_ip_addr=127.0.0.1 # attribute. #ap_settings=hostapd.ap_settings +# WPS UPnP interface +# If set, support for external Registrars is enabled. +#upnp_iface=br0 + +# Friendly Name (required for UPnP) +# Short description for end use. Should be less than 64 characters. +#friendly_name=WPS Access Point + +# Manufacturer URL (optional for UPnP) +#manufacturer_url=http://www.example.com/ + +# Model Description (recommended for UPnP) +# Long description for end user. Should be less than 128 characters. +#model_description=Wireless Access Point + +# Model URL (optional for UPnP) +#model_url=http://www.example.com/model/ + +# Universal Product Code (optional for UPnP) +# 12-digit, all-numeric code that identifies the consumer package. +#upc=123456789012 + ##### Multiple BSSID support ################################################## # # Above configuration is using the default interface (wlan#, or multi-SSID VLAN diff --git a/hostapd/hostapd.h b/hostapd/hostapd.h index b9b345b42..c7b19472b 100644 --- a/hostapd/hostapd.h +++ b/hostapd/hostapd.h @@ -25,6 +25,7 @@ struct wpa_driver_ops; struct wpa_ctrl_dst; struct radius_server_data; +struct upnp_wps_device_sm; #ifdef CONFIG_FULL_DYNAMIC_VLAN struct full_dynamic_vlan; @@ -96,6 +97,7 @@ struct hostapd_data { u8 *wps_probe_resp_ie; size_t wps_probe_resp_ie_len; unsigned int ap_pin_failures; + struct upnp_wps_device_sm *wps_upnp; #endif /* CONFIG_WPS */ }; diff --git a/hostapd/wps_hostapd.c b/hostapd/wps_hostapd.c index 2d9654456..5f7dd897b 100644 --- a/hostapd/wps_hostapd.c +++ b/hostapd/wps_hostapd.c @@ -20,12 +20,22 @@ #include "uuid.h" #include "wpa_ctrl.h" #include "ieee802_11_defs.h" +#include "sta_info.h" +#include "eapol_sm.h" #include "wps/wps.h" #include "wps/wps_defs.h" #include "wps/wps_dev_attr.h" #include "wps_hostapd.h" +#ifdef CONFIG_WPS_UPNP +#include "wps/wps_upnp.h" +static int hostapd_wps_upnp_init(struct hostapd_data *hapd, + struct wps_context *wps); +static void hostapd_wps_upnp_deinit(struct hostapd_data *hapd); +#endif /* CONFIG_WPS_UPNP */ + + static int hostapd_wps_new_psk_cb(void *ctx, const u8 *mac_addr, const u8 *psk, size_t psk_len) { @@ -619,6 +629,22 @@ int hostapd_init_wps(struct hostapd_data *hapd, return -1; } +#ifdef CONFIG_WPS_UPNP + wps->friendly_name = hapd->conf->friendly_name; + wps->manufacturer_url = hapd->conf->manufacturer_url; + wps->model_description = hapd->conf->model_description; + wps->model_url = hapd->conf->model_url; + wps->upc = hapd->conf->upc; + + if (hostapd_wps_upnp_init(hapd, wps) < 0) { + wpa_printf(MSG_ERROR, "Failed to initialize WPS UPnP"); + wps_registrar_deinit(wps->registrar); + os_free(wps->network_key); + os_free(wps); + return -1; + } +#endif /* CONFIG_WPS_UPNP */ + hapd->wps = wps; return 0; @@ -629,9 +655,13 @@ void hostapd_deinit_wps(struct hostapd_data *hapd) { if (hapd->wps == NULL) return; +#ifdef CONFIG_WPS_UPNP + hostapd_wps_upnp_deinit(hapd); +#endif /* CONFIG_WPS_UPNP */ wps_registrar_deinit(hapd->wps->registrar); os_free(hapd->wps->network_key); wps_device_data_free(&hapd->wps->dev); + wpabuf_free(hapd->wps->upnp_msg); os_free(hapd->wps); hapd->wps = NULL; hostapd_wps_clear_ies(hapd); @@ -705,8 +735,244 @@ void hostapd_wps_probe_req_rx(struct hostapd_data *hapd, const u8 *addr, pos += 2 + pos[1]; } - if (wpabuf_len(wps_ie) > 0) + if (wpabuf_len(wps_ie) > 0) { wps_registrar_probe_req_rx(hapd->wps->registrar, addr, wps_ie); +#ifdef CONFIG_WPS_UPNP + /* FIX: what exactly should be included in the WLANEvent? + * WPS attributes? Full ProbeReq frame? */ + upnp_wps_device_send_wlan_event(hapd->wps_upnp, addr, + UPNP_WPS_WLANEVENT_TYPE_PROBE, + wps_ie); +#endif /* CONFIG_WPS_UPNP */ + } wpabuf_free(wps_ie); } + + +#ifdef CONFIG_WPS_UPNP + +static struct wpabuf * +hostapd_rx_req_get_device_info(void *priv, struct upnp_wps_peer *peer) +{ + struct hostapd_data *hapd = priv; + struct wps_config cfg; + struct wps_data *wps; + enum wsc_op_code op_code; + struct wpabuf *m1; + + /* + * 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 = hapd->wps; + cfg.pin = (u8 *) hapd->conf->ap_pin; + cfg.pin_len = os_strlen(hapd->conf->ap_pin); + wps = wps_init(&cfg); + if (wps == NULL) + return NULL; + + m1 = wps_get_msg(wps, &op_code); + if (m1 == NULL) { + wps_deinit(wps); + return NULL; + } + + peer->wps = wps; + + return m1; +} + + +static struct wpabuf * +hostapd_rx_req_put_message(void *priv, struct upnp_wps_peer *peer, + const struct wpabuf *msg) +{ + enum wps_process_res res; + enum wsc_op_code op_code; + + /* PutMessage: msg = InMessage, return OutMessage */ + res = wps_process_msg(peer->wps, WSC_UPnP, msg); + if (res == WPS_FAILURE) + return NULL; + return wps_get_msg(peer->wps, &op_code); +} + + +static struct wpabuf * +hostapd_rx_req_get_ap_settings(void *priv, const struct wpabuf *msg) +{ + wpa_printf(MSG_DEBUG, "WPS UPnP: TODO %s", __func__); + return NULL; +} + + +static int hostapd_rx_req_set_ap_settings(void *priv, const struct wpabuf *msg) +{ + wpa_printf(MSG_DEBUG, "WPS UPnP: TODO %s", __func__); + return -1; +} + + +static int hostapd_rx_req_del_ap_settings(void *priv, const struct wpabuf *msg) +{ + wpa_printf(MSG_DEBUG, "WPS UPnP: TODO %s", __func__); + return -1; +} + + +static struct wpabuf * +hostapd_rx_req_get_sta_settings(void *priv, const struct wpabuf *msg) +{ + wpa_printf(MSG_DEBUG, "WPS UPnP: TODO %s", __func__); + return NULL; +} + + +static int hostapd_rx_req_set_sta_settings(void *priv, + const struct wpabuf *msg) +{ + wpa_printf(MSG_DEBUG, "WPS UPnP: TODO %s", __func__); + return -1; +} + + +static int hostapd_rx_req_del_sta_settings(void *priv, + const struct wpabuf *msg) +{ + wpa_printf(MSG_DEBUG, "WPS UPnP: TODO %s", __func__); + return -1; +} + + +static int hostapd_rx_req_put_wlan_event_response( + void *priv, enum upnp_wps_wlanevent_type ev_type, + const u8 *mac_addr, const struct wpabuf *msg) +{ + struct hostapd_data *hapd = priv; + struct sta_info *sta; + + wpa_printf(MSG_DEBUG, "WPS UPnP: PutWLANResponse ev_type=%d mac_addr=" + MACSTR, ev_type, MAC2STR(mac_addr)); + wpa_hexdump_ascii(MSG_MSGDUMP, "WPS UPnP: PutWLANResponse NewMessage", + wpabuf_head(msg), wpabuf_len(msg)); + if (ev_type != UPNP_WPS_WLANEVENT_TYPE_EAP) { + wpa_printf(MSG_DEBUG, "WPS UPnP: Ignored unexpected " + "PutWLANResponse WLANEventType %d", ev_type); + return -1; + } + + /* + * EAP response to ongoing to WPS Registration. Send it to EAP-WSC + * server implementation for delivery to the peer. + */ + + /* TODO: support multiple pending UPnP messages */ + os_memcpy(hapd->wps->upnp_msg_addr, mac_addr, ETH_ALEN); + wpabuf_free(hapd->wps->upnp_msg); + hapd->wps->upnp_msg = wpabuf_dup(msg); + + sta = ap_get_sta(hapd, mac_addr); + if (sta) + return eapol_auth_eap_pending_cb(sta->eapol_sm, + hapd->wps->pending_session); + + return 0; +} + + +static int hostapd_rx_req_set_selected_registrar(void *priv, + const struct wpabuf *msg) +{ + struct hostapd_data *hapd = priv; + return wps_registrar_set_selected_registrar(hapd->wps->registrar, msg); +} + + +static int hostapd_rx_req_reboot_ap(void *priv, const struct wpabuf *msg) +{ + wpa_printf(MSG_DEBUG, "WPS UPnP: TODO %s", __func__); + return -1; +} + + +static int hostapd_rx_req_reset_ap(void *priv, const struct wpabuf *msg) +{ + wpa_printf(MSG_DEBUG, "WPS UPnP: TODO %s", __func__); + return -1; +} + + +static int hostapd_rx_req_reboot_sta(void *priv, const struct wpabuf *msg) +{ + wpa_printf(MSG_DEBUG, "WPS UPnP: TODO %s", __func__); + return -1; +} + + +static int hostapd_rx_req_reset_sta(void *priv, const struct wpabuf *msg) +{ + wpa_printf(MSG_DEBUG, "WPS UPnP: TODO %s", __func__); + return -1; +} + + +static int hostapd_wps_upnp_init(struct hostapd_data *hapd, + struct wps_context *wps) +{ + struct upnp_wps_device_ctx *ctx; + + if (!hapd->conf->upnp_iface) + return 0; + ctx = os_zalloc(sizeof(*ctx)); + if (ctx == NULL) + return -1; + + ctx->rx_req_get_device_info = hostapd_rx_req_get_device_info; + ctx->rx_req_put_message = hostapd_rx_req_put_message; + ctx->rx_req_get_ap_settings = hostapd_rx_req_get_ap_settings; + ctx->rx_req_set_ap_settings = hostapd_rx_req_set_ap_settings; + ctx->rx_req_del_ap_settings = hostapd_rx_req_del_ap_settings; + ctx->rx_req_get_sta_settings = hostapd_rx_req_get_sta_settings; + ctx->rx_req_set_sta_settings = hostapd_rx_req_set_sta_settings; + ctx->rx_req_del_sta_settings = hostapd_rx_req_del_sta_settings; + ctx->rx_req_put_wlan_event_response = + hostapd_rx_req_put_wlan_event_response; + ctx->rx_req_set_selected_registrar = + hostapd_rx_req_set_selected_registrar; + ctx->rx_req_reboot_ap = hostapd_rx_req_reboot_ap; + ctx->rx_req_reset_ap = hostapd_rx_req_reset_ap; + ctx->rx_req_reboot_sta = hostapd_rx_req_reboot_sta; + ctx->rx_req_reset_sta = hostapd_rx_req_reset_sta; + + hapd->wps_upnp = upnp_wps_device_init(ctx, wps, hapd); + if (hapd->wps_upnp == NULL) { + os_free(ctx); + return -1; + } + wps->wps_upnp = hapd->wps_upnp; + + if (upnp_wps_device_start(hapd->wps_upnp, hapd->conf->upnp_iface)) { + upnp_wps_device_deinit(hapd->wps_upnp); + hapd->wps_upnp = NULL; + return -1; + } + + return 0; +} + + +static void hostapd_wps_upnp_deinit(struct hostapd_data *hapd) +{ + upnp_wps_device_deinit(hapd->wps_upnp); +} + +#endif /* CONFIG_WPS_UPNP */ diff --git a/src/eap_peer/eap_wsc.c b/src/eap_peer/eap_wsc.c index 35c9cce45..17e42f477 100644 --- a/src/eap_peer/eap_wsc.c +++ b/src/eap_peer/eap_wsc.c @@ -405,6 +405,7 @@ static struct wpabuf * eap_wsc_process(struct eap_sm *sm, void *priv, eap_wsc_state(data, MESG); break; case WPS_FAILURE: + case WPS_PENDING: wpa_printf(MSG_DEBUG, "EAP-WSC: WPS processing failed"); eap_wsc_state(data, FAIL); break; diff --git a/src/eap_server/eap_wsc.c b/src/eap_server/eap_wsc.c index c22c54407..097e8c4f6 100644 --- a/src/eap_server/eap_wsc.c +++ b/src/eap_server/eap_wsc.c @@ -15,6 +15,7 @@ #include "includes.h" #include "common.h" +#include "eloop.h" #include "eap_i.h" #include "eap_common/eap_wsc_common.h" #include "wps/wps.h" @@ -29,6 +30,7 @@ struct eap_wsc_data { size_t out_used; size_t fragment_size; struct wps_data *wps; + int ext_reg_timeout; }; @@ -62,6 +64,21 @@ static void eap_wsc_state(struct eap_wsc_data *data, int state) } +static void eap_wsc_ext_reg_timeout(void *eloop_ctx, void *timeout_ctx) +{ + struct eap_sm *sm = eloop_ctx; + struct eap_wsc_data *data = timeout_ctx; + + if (sm->method_pending != METHOD_PENDING_WAIT) + return; + + wpa_printf(MSG_DEBUG, "EAP-WSC: Timeout while waiting for an External " + "Registrar"); + data->ext_reg_timeout = 1; + eap_sm_pending_cb(sm); +} + + static void * eap_wsc_init(struct eap_sm *sm) { struct eap_wsc_data *data; @@ -123,6 +140,7 @@ static void * eap_wsc_init(struct eap_sm *sm) static void eap_wsc_reset(struct eap_sm *sm, void *priv) { struct eap_wsc_data *data = priv; + eloop_cancel_timeout(eap_wsc_ext_reg_timeout, sm, data); wpabuf_free(data->in_buf); wpabuf_free(data->out_buf); wps_deinit(data->wps); @@ -324,6 +342,12 @@ static void eap_wsc_process(struct eap_sm *sm, void *priv, enum wps_process_res res; struct wpabuf tmpbuf; + eloop_cancel_timeout(eap_wsc_ext_reg_timeout, sm, data); + if (data->ext_reg_timeout) { + eap_wsc_state(data, FAIL); + return; + } + pos = eap_hdr_validate(EAP_VENDOR_WFA, EAP_VENDOR_TYPE_WSC, respData, &len); if (pos == NULL || len < 2) @@ -409,6 +433,14 @@ static void eap_wsc_process(struct eap_sm *sm, void *priv, wpa_printf(MSG_DEBUG, "EAP-WSC: WPS processing failed"); eap_wsc_state(data, FAIL); break; + case WPS_PENDING: + eap_wsc_state(data, MSG); + sm->method_pending = METHOD_PENDING_WAIT; + sm->wps->pending_session = sm; + eloop_cancel_timeout(eap_wsc_ext_reg_timeout, sm, data); + eloop_register_timeout(5, 0, eap_wsc_ext_reg_timeout, + sm, data); + break; } if (data->in_buf != &tmpbuf) diff --git a/src/utils/wpabuf.c b/src/utils/wpabuf.c index e809690b9..c5441790e 100644 --- a/src/utils/wpabuf.c +++ b/src/utils/wpabuf.c @@ -1,6 +1,6 @@ /* * Dynamic data buffer - * Copyright (c) 2007-2008, Jouni Malinen + * Copyright (c) 2007-2009, Jouni Malinen * * 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 @@ -195,3 +195,18 @@ struct wpabuf * wpabuf_zeropad(struct wpabuf *buf, size_t len) return ret; } + + +void wpabuf_printf(struct wpabuf *buf, char *fmt, ...) +{ + va_list ap; + void *tmp = wpabuf_mhead_u8(buf) + wpabuf_len(buf); + int res; + + va_start(ap, fmt); + res = vsnprintf(tmp, buf->size - buf->used, fmt, ap); + va_end(ap); + if (res < 0 || (size_t) res >= buf->size - buf->used) + wpabuf_overflow(buf, res); + buf->used += res; +} diff --git a/src/utils/wpabuf.h b/src/utils/wpabuf.h index 5d435ab10..bd8f09e94 100644 --- a/src/utils/wpabuf.h +++ b/src/utils/wpabuf.h @@ -1,6 +1,6 @@ /* * Dynamic data buffer - * Copyright (c) 2007, Jouni Malinen + * Copyright (c) 2007-2009, Jouni Malinen * * 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 @@ -38,6 +38,7 @@ void wpabuf_free(struct wpabuf *buf); void * wpabuf_put(struct wpabuf *buf, size_t len); struct wpabuf * wpabuf_concat(struct wpabuf *a, struct wpabuf *b); struct wpabuf * wpabuf_zeropad(struct wpabuf *buf, size_t len); +void wpabuf_printf(struct wpabuf *buf, char *fmt, ...) PRINTF_FORMAT(2, 3); /** @@ -147,4 +148,9 @@ static inline void wpabuf_set(struct wpabuf *buf, const void *data, size_t len) buf->size = buf->used = len; } +static inline void wpabuf_put_str(struct wpabuf *dst, const char *str) +{ + wpabuf_put_data(dst, str, os_strlen(str)); +} + #endif /* WPABUF_H */ diff --git a/src/wps/httpread.c b/src/wps/httpread.c new file mode 100644 index 000000000..313b4687c --- /dev/null +++ b/src/wps/httpread.c @@ -0,0 +1,858 @@ +/** + * httpread - Manage reading file(s) from HTTP/TCP socket + * Author: Ted Merrill + * Copyright 2008 Atheros Communications + * + * 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. + * + * The files are buffered via internal callbacks from eloop, then presented to + * an application callback routine when completely read into memory. May also + * be used if no file is expected but just to get the header, including HTTP + * replies (e.g. HTTP/1.1 200 OK etc.). + * + * This does not attempt to be an optimally efficient implementation, but does + * attempt to be of reasonably small size and memory consumption; assuming that + * only small files are to be read. A maximum file size is provided by + * application and enforced. + * + * It is assumed that the application does not expect any of the following: + * -- transfer encoding other than chunked + * -- trailer fields + * It is assumed that, even if the other side requested that the connection be + * kept open, that we will close it (thus HTTP messages sent by application + * should have the connection closed field); this is allowed by HTTP/1.1 and + * simplifies things for us. + * + * Other limitations: + * -- HTTP header may not exceed a hard-coded size. + * + * Notes: + * This code would be massively simpler without some of the new features of + * HTTP/1.1, especially chunked data. + */ + +#include "includes.h" + +#include "common.h" +#include "eloop.h" +#include "httpread.h" + + +/* Tunable parameters */ +#define HTTPREAD_READBUF_SIZE 1024 /* read in chunks of this size */ +#define HTTPREAD_HEADER_MAX_SIZE 4096 /* max allowed for headers */ +#define HTTPREAD_BODYBUF_DELTA 4096 /* increase allocation by this */ + +#if 0 +/* httpread_debug -- set this global variable > 0 e.g. from debugger + * to enable debugs (larger numbers for more debugs) + * Make this a #define of 0 to eliminate the debugging code. + */ +int httpread_debug = 99; +#else +#define httpread_debug 0 /* eliminates even the debugging code */ +#endif + + +/* control instance -- actual definition (opaque to application) + */ +struct httpread { + /* information from creation */ + int sd; /* descriptor of TCP socket to read from */ + void (*cb)(struct httpread *handle, void *cookie, + enum httpread_event e); /* call on event */ + void *cookie; /* pass to callback */ + int max_bytes; /* maximum file size else abort it */ + int timeout_seconds; /* 0 or total duration timeout period */ + + /* dynamically used information follows */ + int sd_registered; /* nonzero if we need to unregister socket */ + int to_registered; /* nonzero if we need to unregister timeout */ + + int got_hdr; /* nonzero when header is finalized */ + char hdr[HTTPREAD_HEADER_MAX_SIZE+1]; /* headers stored here */ + int hdr_nbytes; + + enum httpread_hdr_type hdr_type; + int version; /* 1 if we've seen 1.1 */ + int reply_code; /* for type REPLY, e.g. 200 for HTTP/1.1 200 OK */ + int got_content_length; /* true if we know content length for sure */ + int content_length; /* body length, iff got_content_length */ + int chunked; /* nonzero for chunked data */ + char *uri; + + int got_body; /* nonzero when body is finalized */ + char *body; + int body_nbytes; + int body_alloc_nbytes; /* amount allocated */ + + int got_file; /* here when we are done */ + + /* The following apply if data is chunked: */ + int in_chunk_data; /* 0=in/at header, 1=in the data or tail*/ + int chunk_start; /* offset in body of chunk hdr or data */ + int chunk_size; /* data of chunk (not hdr or ending CRLF)*/ + int in_trailer; /* in header fields after data (chunked only)*/ + enum trailer_state { + trailer_line_begin = 0, + trailer_empty_cr, /* empty line + CR */ + trailer_nonempty, + trailer_nonempty_cr, + } trailer_state; +}; + + +/* Check words for equality, where words consist of graphical characters + * delimited by whitespace + * Returns nonzero if "equal" doing case insensitive comparison. + */ +static int word_eq(char *s1, char *s2) +{ + int c1; + int c2; + int end1 = 0; + int end2 = 0; + for (;;) { + c1 = *s1++; + c2 = *s2++; + if (isalpha(c1) && isupper(c1)) + c1 = tolower(c1); + if (isalpha(c2) && isupper(c2)) + c2 = tolower(c2); + end1 = !isgraph(c1); + end2 = !isgraph(c2); + if (end1 || end2 || c1 != c2) + break; + } + return end1 && end2; /* reached end of both words? */ +} + + +/* convert hex to binary + * Requires that c have been previously tested true with isxdigit(). + */ +static int hex_value(int c) +{ + if (isdigit(c)) + return c - '0'; + if (islower(c)) + return 10 + c - 'a'; + return 10 + c - 'A'; +} + + +static void httpread_timeout_handler(void *eloop_data, void *user_ctx); + +/* httpread_destroy -- if h is non-NULL, clean up + * This must eventually be called by the application following + * call of the application's callback and may be called + * earlier if desired. + */ +void httpread_destroy(struct httpread *h) +{ + if (httpread_debug >= 10) + wpa_printf(MSG_DEBUG, "ENTER httpread_destroy(%p)", h); + if (!h) + return; + + if (h->to_registered) + eloop_cancel_timeout(httpread_timeout_handler, NULL, h); + h->to_registered = 0; + if (h->sd_registered) + eloop_unregister_sock(h->sd, EVENT_TYPE_READ); + h->sd_registered = 0; + os_free(h->body); + os_free(h->uri); + os_memset(h, 0, sizeof(*h)); /* aid debugging */ + h->sd = -1; /* aid debugging */ + os_free(h); +} + + +/* httpread_timeout_handler -- called on excessive total duration + */ +static void httpread_timeout_handler(void *eloop_data, void *user_ctx) +{ + struct httpread *h = user_ctx; + wpa_printf(MSG_DEBUG, "httpread timeout (%p)", h); + h->to_registered = 0; /* is self-cancelling */ + (*h->cb)(h, h->cookie, HTTPREAD_EVENT_TIMEOUT); +} + + +/* Analyze options only so far as is needed to correctly obtain the file. + * The application can look at the raw header to find other options. + */ +static int httpread_hdr_option_analyze( + struct httpread *h, + char *hbp /* pointer to current line in header buffer */ + ) +{ + if (word_eq(hbp, "CONTENT-LENGTH:")) { + while (isgraph(*hbp)) + hbp++; + while (*hbp == ' ' || *hbp == '\t') + hbp++; + if (!isdigit(*hbp)) + return -1; + h->content_length = atol(hbp); + h->got_content_length = 1; + return 0; + } + if (word_eq(hbp, "TRANSFER_ENCODING:")) { + while (isgraph(*hbp)) + hbp++; + while (*hbp == ' ' || *hbp == '\t') + hbp++; + /* There should (?) be no encodings of interest + * other than chunked... + */ + if (os_strncmp(hbp, "CHUNKED", 7)) { + h->chunked = 1; + h->in_chunk_data = 0; + /* ignore possible ; */ + } + return 0; + } + /* skip anything we don't know, which is a lot */ + return 0; +} + + +static int httpread_hdr_analyze(struct httpread *h) +{ + char *hbp = h->hdr; /* pointer into h->hdr */ + int standard_first_line = 1; + + /* First line is special */ + h->hdr_type = HTTPREAD_HDR_TYPE_UNKNOWN; + if (!isgraph(*hbp)) + goto bad; + if (os_strncmp(hbp, "HTTP/", 5) == 0) { + h->hdr_type = HTTPREAD_HDR_TYPE_REPLY; + standard_first_line = 0; + hbp += 5; + if (hbp[0] == '1' && hbp[1] == '.' && + isdigit(hbp[2]) && hbp[2] != '0') + h->version = 1; + while (isgraph(*hbp)) + hbp++; + while (*hbp == ' ' || *hbp == '\t') + hbp++; + if (!isdigit(*hbp)) + goto bad; + h->reply_code = atol(hbp); + } else if (word_eq(hbp, "GET")) + h->hdr_type = HTTPREAD_HDR_TYPE_GET; + else if (word_eq(hbp, "HEAD")) + h->hdr_type = HTTPREAD_HDR_TYPE_HEAD; + else if (word_eq(hbp, "POST")) + h->hdr_type = HTTPREAD_HDR_TYPE_POST; + else if (word_eq(hbp, "PUT")) + h->hdr_type = HTTPREAD_HDR_TYPE_PUT; + else if (word_eq(hbp, "DELETE")) + h->hdr_type = HTTPREAD_HDR_TYPE_DELETE; + else if (word_eq(hbp, "TRACE")) + h->hdr_type = HTTPREAD_HDR_TYPE_TRACE; + else if (word_eq(hbp, "CONNECT")) + h->hdr_type = HTTPREAD_HDR_TYPE_CONNECT; + else if (word_eq(hbp, "NOTIFY")) + h->hdr_type = HTTPREAD_HDR_TYPE_NOTIFY; + else if (word_eq(hbp, "M-SEARCH")) + h->hdr_type = HTTPREAD_HDR_TYPE_M_SEARCH; + else if (word_eq(hbp, "M-POST")) + h->hdr_type = HTTPREAD_HDR_TYPE_M_POST; + else if (word_eq(hbp, "SUBSCRIBE")) + h->hdr_type = HTTPREAD_HDR_TYPE_SUBSCRIBE; + else if (word_eq(hbp, "UNSUBSCRIBE")) + h->hdr_type = HTTPREAD_HDR_TYPE_UNSUBSCRIBE; + else { + } + + if (standard_first_line) { + char *rawuri; + char *uri; + /* skip type */ + while (isgraph(*hbp)) + hbp++; + while (*hbp == ' ' || *hbp == '\t') + hbp++; + /* parse uri. + * Find length, allocate memory for translated + * copy, then translate by changing % + * into represented value. + */ + rawuri = hbp; + while (isgraph(*hbp)) + hbp++; + h->uri = os_malloc((hbp - rawuri) + 1); + if (h->uri == NULL) + goto bad; + uri = h->uri; + while (rawuri < hbp) { + int c = *rawuri; + if (c == '%' && + isxdigit(rawuri[1]) && isxdigit(rawuri[2])) { + *uri++ = (hex_value(rawuri[1]) << 4) | + hex_value(rawuri[2]); + rawuri += 3; + } else { + *uri++ = c; + rawuri++; + } + } + *uri = 0; /* null terminate */ + while (isgraph(*hbp)) + hbp++; + while (*hbp == ' ' || *hbp == '\t') + hbp++; + /* get version */ + if (0 == strncmp(hbp, "HTTP/", 5)) { + hbp += 5; + if (hbp[0] == '1' && hbp[1] == '.' && + isdigit(hbp[2]) && hbp[2] != '0') + h->version = 1; + } + } + /* skip rest of line */ + while (*hbp) + if (*hbp++ == '\n') + break; + + /* Remainder of lines are options, in any order; + * or empty line to terminate + */ + for (;;) { + /* Empty line to terminate */ + if (hbp[0] == '\n' || + (hbp[0] == '\r' && hbp[1] == '\n')) + break; + if (!isgraph(*hbp)) + goto bad; + if (httpread_hdr_option_analyze(h, hbp)) + goto bad; + /* skip line */ + while (*hbp) + if (*hbp++ == '\n') + break; + } + + /* chunked overrides content-length always */ + if (h->chunked) + h->got_content_length = 0; + + /* For some types, we should not try to read a body + * This is in addition to the application determining + * that we should not read a body. + */ + switch (h->hdr_type) { + case HTTPREAD_HDR_TYPE_REPLY: + /* Some codes can have a body and some not. + * For now, just assume that any other than 200 + * do not... + */ + if (h->reply_code != 200) + h->max_bytes = 0; + break; + case HTTPREAD_HDR_TYPE_GET: + case HTTPREAD_HDR_TYPE_HEAD: + /* in practice it appears that it is assumed + * that GETs have a body length of 0... ? + */ + if (h->chunked == 0 && h->got_content_length == 0) + h->max_bytes = 0; + break; + case HTTPREAD_HDR_TYPE_POST: + case HTTPREAD_HDR_TYPE_PUT: + case HTTPREAD_HDR_TYPE_DELETE: + case HTTPREAD_HDR_TYPE_TRACE: + case HTTPREAD_HDR_TYPE_CONNECT: + case HTTPREAD_HDR_TYPE_NOTIFY: + case HTTPREAD_HDR_TYPE_M_SEARCH: + case HTTPREAD_HDR_TYPE_M_POST: + case HTTPREAD_HDR_TYPE_SUBSCRIBE: + case HTTPREAD_HDR_TYPE_UNSUBSCRIBE: + default: + break; + } + + return 0; + +bad: + /* Error */ + return -1; +} + + +/* httpread_read_handler -- called when socket ready to read + * + * Note: any extra data we read past end of transmitted file is ignored; + * if we were to support keeping connections open for multiple files then + * this would have to be addressed. + */ +static void httpread_read_handler(int sd, void *eloop_ctx, void *sock_ctx) +{ + struct httpread *h = sock_ctx; + int nread; + char *rbp; /* pointer into read buffer */ + char *hbp; /* pointer into header buffer */ + char *bbp; /* pointer into body buffer */ + char readbuf[HTTPREAD_READBUF_SIZE]; /* temp use to read into */ + + if (httpread_debug >= 20) + wpa_printf(MSG_DEBUG, "ENTER httpread_read_handler(%p)", h); + + /* read some at a time, then search for the interal + * boundaries between header and data and etc. + */ + nread = read(h->sd, readbuf, sizeof(readbuf)); + if (nread < 0) + goto bad; + if (nread == 0) { + /* end of transmission... this may be normal + * or may be an error... in some cases we can't + * tell which so we must assume it is normal then. + */ + if (!h->got_hdr) { + /* Must at least have completed header */ + wpa_printf(MSG_DEBUG, "httpread premature eof(%p)", h); + goto bad; + } + if (h->chunked || h->got_content_length) { + /* Premature EOF; e.g. dropped connection */ + wpa_printf(MSG_DEBUG, + "httpread premature eof(%p) %d/%d", + h, h->body_nbytes, + h->content_length); + goto bad; + } + /* No explicit length, hopefully we have all the data + * although dropped connections can cause false + * end + */ + if (httpread_debug >= 10) + wpa_printf(MSG_DEBUG, "httpread ok eof(%p)", h); + h->got_body = 1; + goto got_file; + } + rbp = readbuf; + + /* Header consists of text lines (terminated by both CR and LF) + * and an empty line (CR LF only). + */ + if (!h->got_hdr) { + hbp = h->hdr + h->hdr_nbytes; + /* add to headers until: + * -- we run out of data in read buffer + * -- or, we run out of header buffer room + * -- or, we get double CRLF in headers + */ + for (;;) { + if (nread == 0) + goto get_more; + if (h->hdr_nbytes == HTTPREAD_HEADER_MAX_SIZE) { + goto bad; + } + *hbp++ = *rbp++; + nread--; + h->hdr_nbytes++; + if (h->hdr_nbytes >= 4 && + hbp[-1] == '\n' && + hbp[-2] == '\r' && + hbp[-3] == '\n' && + hbp[-4] == '\r' ) { + h->got_hdr = 1; + *hbp = 0; /* null terminate */ + break; + } + } + /* here we've just finished reading the header */ + if (httpread_hdr_analyze(h)) { + wpa_printf(MSG_DEBUG, "httpread bad hdr(%p)", h); + goto bad; + } + if (h->max_bytes == 0) { + if (httpread_debug >= 10) + wpa_printf(MSG_DEBUG, + "httpread no body hdr end(%p)", h); + goto got_file; + } + if (h->got_content_length && h->content_length == 0) { + if (httpread_debug >= 10) + wpa_printf(MSG_DEBUG, + "httpread zero content length(%p)", + h); + goto got_file; + } + } + + /* Certain types of requests never have data and so + * must be specially recognized. + */ + if (!os_strncasecmp(h->hdr, "SUBSCRIBE", 9) || + !os_strncasecmp(h->hdr, "UNSUBSCRIBE", 11) || + !os_strncasecmp(h->hdr, "HEAD", 4) || + !os_strncasecmp(h->hdr, "GET", 3)) { + if (!h->got_body) { + if (httpread_debug >= 10) + wpa_printf(MSG_DEBUG, + "httpread NO BODY for sp. type"); + } + h->got_body = 1; + goto got_file; + } + + /* Data can be just plain binary data, or if "chunked" + * consists of chunks each with a header, ending with + * an ending header. + */ + if (!h->got_body) { + /* Here to get (more of) body */ + /* ensure we have enough room for worst case for body + * plus a null termination character + */ + if (h->body_alloc_nbytes < (h->body_nbytes + nread + 1)) { + char *new_body; + int new_alloc_nbytes; + + if (h->body_nbytes >= h->max_bytes) + goto bad; + new_alloc_nbytes = h->body_alloc_nbytes + + HTTPREAD_BODYBUF_DELTA; + /* For content-length case, the first time + * through we allocate the whole amount + * we need. + */ + if (h->got_content_length && + new_alloc_nbytes < (h->content_length + 1)) + new_alloc_nbytes = h->content_length + 1; + if ((new_body = os_realloc(h->body, new_alloc_nbytes)) + == NULL) + goto bad; + + h->body = new_body; + h->body_alloc_nbytes = new_alloc_nbytes; + } + /* add bytes */ + bbp = h->body + h->body_nbytes; + for (;;) { + int ncopy; + /* See if we need to stop */ + if (h->chunked && h->in_chunk_data == 0) { + /* in chunk header */ + char *cbp = h->body + h->chunk_start; + if (bbp-cbp >= 2 && bbp[-2] == '\r' && + bbp[-1] == '\n') { + /* end of chunk hdr line */ + /* hdr line consists solely + * of a hex numeral and CFLF + */ + if (!isxdigit(*cbp)) + goto bad; + h->chunk_size = strtoul(cbp, NULL, 16); + /* throw away chunk header + * so we have only real data + */ + h->body_nbytes = h->chunk_start; + bbp = cbp; + if (h->chunk_size == 0) { + /* end of chunking */ + /* trailer follows */ + h->in_trailer = 1; + if (httpread_debug >= 20) + wpa_printf( + MSG_DEBUG, + "httpread end chunks(%p)", h); + break; + } + h->in_chunk_data = 1; + /* leave chunk_start alone */ + } + } else if (h->chunked) { + /* in chunk data */ + if ((h->body_nbytes - h->chunk_start) == + (h->chunk_size + 2)) { + /* end of chunk reached, + * new chunk starts + */ + /* check chunk ended w/ CRLF + * which we'll throw away + */ + if (bbp[-1] == '\n' && + bbp[-2] == '\r') { + } else + goto bad; + h->body_nbytes -= 2; + bbp -= 2; + h->chunk_start = h->body_nbytes; + h->in_chunk_data = 0; + h->chunk_size = 0; /* just in case */ + } + } else if (h->got_content_length && + h->body_nbytes >= h->content_length) { + h->got_body = 1; + if (httpread_debug >= 10) + wpa_printf( + MSG_DEBUG, + "httpread got content(%p)", h); + goto got_file; + } + if (nread <= 0) + break; + /* Now transfer. Optimize using memcpy where we can. */ + if (h->chunked && h->in_chunk_data) { + /* copy up to remainder of chunk data + * plus the required CR+LF at end + */ + ncopy = (h->chunk_start + h->chunk_size + 2) - + h->body_nbytes; + } else if (h->chunked) { + /*in chunk header -- don't optimize */ + *bbp++ = *rbp++; + nread--; + h->body_nbytes++; + continue; + } else if (h->got_content_length) { + ncopy = h->content_length - h->body_nbytes; + } else { + ncopy = nread; + } + /* Note: should never be 0 */ + if (ncopy > nread) + ncopy = nread; + os_memcpy(bbp, rbp, ncopy); + bbp += ncopy; + h->body_nbytes += ncopy; + rbp += ncopy; + nread -= ncopy; + } /* body copy loop */ + } /* !got_body */ + if (h->chunked && h->in_trailer) { + /* If "chunked" then there is always a trailer, + * consisting of zero or more non-empty lines + * ending with CR LF and then an empty line w/ CR LF. + * We do NOT support trailers except to skip them -- + * this is supported (generally) by the http spec. + */ + bbp = h->body + h->body_nbytes; + for (;;) { + int c; + if (nread <= 0) + break; + c = *rbp++; + nread--; + switch (h->trailer_state) { + case trailer_line_begin: + if (c == '\r') + h->trailer_state = trailer_empty_cr; + else + h->trailer_state = trailer_nonempty; + break; + case trailer_empty_cr: + /* end empty line */ + if (c == '\n') { + h->trailer_state = trailer_line_begin; + h->in_trailer = 0; + if (httpread_debug >= 10) + wpa_printf( + MSG_DEBUG, + "httpread got content(%p)", h); + h->got_body = 1; + goto got_file; + } + h->trailer_state = trailer_nonempty; + break; + case trailer_nonempty: + if (c == '\r') + h->trailer_state = trailer_nonempty_cr; + break; + case trailer_nonempty_cr: + if (c == '\n') + h->trailer_state = trailer_line_begin; + else + h->trailer_state = trailer_nonempty; + break; + } + } + } + goto get_more; + +bad: + /* Error */ + wpa_printf(MSG_DEBUG, "httpread read/parse failure (%p)", h); + (*h->cb)(h, h->cookie, HTTPREAD_EVENT_ERROR); + return; + +get_more: + return; + +got_file: + if (httpread_debug >= 10) + wpa_printf(MSG_DEBUG, + "httpread got file %d bytes type %d", + h->body_nbytes, h->hdr_type); + /* Null terminate for convenience of some applications */ + if (h->body) + h->body[h->body_nbytes] = 0; /* null terminate */ + h->got_file = 1; + /* Assume that we do NOT support keeping connection alive, + * and just in case somehow we don't get destroyed right away, + * unregister now. + */ + if (h->sd_registered) + eloop_unregister_sock(h->sd, EVENT_TYPE_READ); + h->sd_registered = 0; + /* The application can destroy us whenever they feel like... + * cancel timeout. + */ + if (h->to_registered) + eloop_cancel_timeout(httpread_timeout_handler, NULL, h); + h->to_registered = 0; + (*h->cb)(h, h->cookie, HTTPREAD_EVENT_FILE_READY); +} + + +/* httpread_create -- start a new reading session making use of eloop. + * The new instance will use the socket descriptor for reading (until + * it gets a file and not after) but will not close the socket, even + * when the instance is destroyed (the application must do that). + * Return NULL on error. + * + * Provided that httpread_create successfully returns a handle, + * the callback fnc is called to handle httpread_event events. + * The caller should do destroy on any errors or unknown events. + * + * Pass max_bytes == 0 to not read body at all (required for e.g. + * reply to HEAD request). + */ +struct httpread * httpread_create( + int sd, /* descriptor of TCP socket to read from */ + void (*cb)(struct httpread *handle, void *cookie, + enum httpread_event e), /* call on event */ + void *cookie, /* pass to callback */ + int max_bytes, /* maximum body size else abort it */ + int timeout_seconds /* 0; or total duration timeout period */ + ) +{ + struct httpread *h = NULL; + + h = os_zalloc(sizeof(*h)); + if (h == NULL) + goto fail; + h->sd = sd; + h->cb = cb; + h->cookie = cookie; + h->max_bytes = max_bytes; + h->timeout_seconds = timeout_seconds; + + if (timeout_seconds > 0) { + if (eloop_register_timeout(timeout_seconds, 0, + httpread_timeout_handler, + NULL, h)) { + /* No way to recover (from malloc failure) */ + goto fail; + } + h->to_registered = 1; + } + if (eloop_register_sock(sd, EVENT_TYPE_READ, httpread_read_handler, + NULL, h)) { + /* No way to recover (from malloc failure) */ + goto fail; + } + h->sd_registered = 1; + return h; + +fail: + + /* Error */ + httpread_destroy(h); + return NULL; +} + + +/* httpread_hdr_type_get -- When file is ready, returns header type. */ +enum httpread_hdr_type httpread_hdr_type_get(struct httpread *h) +{ + return h->hdr_type; +} + + +/* httpread_uri_get -- When file is ready, uri_get returns (translated) URI + * or possibly NULL (which would be an error). + */ +char * httpread_uri_get(struct httpread *h) +{ + return h->uri; +} + + +/* httpread_reply_code_get -- When reply is ready, returns reply code */ +int httpread_reply_code_get(struct httpread *h) +{ + return h->reply_code; +} + + +/* httpread_length_get -- When file is ready, returns file length. */ +int httpread_length_get(struct httpread *h) +{ + return h->body_nbytes; +} + + +/* httpread_data_get -- When file is ready, returns file content + * with null byte appened. + * Might return NULL in some error condition. + */ +void * httpread_data_get(struct httpread *h) +{ + return h->body ? h->body : ""; +} + + +/* httpread_hdr_get -- When file is ready, returns header content + * with null byte appended. + * Might return NULL in some error condition. + */ +char * httpread_hdr_get(struct httpread *h) +{ + return h->hdr; +} + + +/* httpread_hdr_line_get -- When file is ready, returns pointer + * to line within header content matching the given tag + * (after the tag itself and any spaces/tabs). + * + * The tag should end with a colon for reliable matching. + * + * If not found, returns NULL; + */ +char * httpread_hdr_line_get(struct httpread *h, const char *tag) +{ + int tag_len = os_strlen(tag); + char *hdr = h->hdr; + hdr = os_strchr(hdr, '\n'); + if (hdr == NULL) + return NULL; + hdr++; + for (;;) { + if (!os_strncasecmp(hdr, tag, tag_len)) { + hdr += tag_len; + while (*hdr == ' ' || *hdr == '\t') + hdr++; + return hdr; + } + hdr = os_strchr(hdr, '\n'); + if (hdr == NULL) + return NULL; + hdr++; + } +} diff --git a/src/wps/httpread.h b/src/wps/httpread.h new file mode 100644 index 000000000..fb1ecb7de --- /dev/null +++ b/src/wps/httpread.h @@ -0,0 +1,123 @@ +/** + * httpread - Manage reading file(s) from HTTP/TCP socket + * Author: Ted Merrill + * Copyright 2008 Atheros Communications + * + * 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. + */ + +#ifndef HTTPREAD_H +#define HTTPREAD_H + +/* event types (passed to callback) */ +enum httpread_event { + HTTPREAD_EVENT_FILE_READY = 1, /* including reply ready */ + HTTPREAD_EVENT_TIMEOUT = 2, + HTTPREAD_EVENT_ERROR = 3 /* misc. error, esp malloc error */ +}; + + +/* header type detected + * available to callback via call to httpread_reply_code_get() + */ +enum httpread_hdr_type { + HTTPREAD_HDR_TYPE_UNKNOWN = 0, /* none of the following */ + HTTPREAD_HDR_TYPE_REPLY = 1, /* hdr begins w/ HTTP/ */ + HTTPREAD_HDR_TYPE_GET = 2, /* hdr begins with GET */ + HTTPREAD_HDR_TYPE_HEAD = 3, /* hdr begins with HEAD */ + HTTPREAD_HDR_TYPE_POST = 4, /* hdr begins with POST */ + HTTPREAD_HDR_TYPE_PUT = 5, /* hdr begins with ... */ + HTTPREAD_HDR_TYPE_DELETE = 6, /* hdr begins with ... */ + HTTPREAD_HDR_TYPE_TRACE = 7, /* hdr begins with ... */ + HTTPREAD_HDR_TYPE_CONNECT = 8, /* hdr begins with ... */ + HTTPREAD_HDR_TYPE_NOTIFY = 9, /* hdr begins with ... */ + HTTPREAD_HDR_TYPE_M_SEARCH = 10, /* hdr begins with ... */ + HTTPREAD_HDR_TYPE_M_POST = 11, /* hdr begins with ... */ + HTTPREAD_HDR_TYPE_SUBSCRIBE = 12, /* hdr begins with ... */ + HTTPREAD_HDR_TYPE_UNSUBSCRIBE = 13, /* hdr begins with ... */ + + HTTPREAD_N_HDR_TYPES /* keep last */ +}; + + +/* control instance -- opaque struct declaration + */ +struct httpread; + + +/* httpread_destroy -- if h is non-NULL, clean up + * This must eventually be called by the application following + * call of the application's callback and may be called + * earlier if desired. + */ +void httpread_destroy(struct httpread *h); + +/* httpread_create -- start a new reading session making use of eloop. + * The new instance will use the socket descriptor for reading (until + * it gets a file and not after) but will not close the socket, even + * when the instance is destroyed (the application must do that). + * Return NULL on error. + * + * Provided that httpread_create successfully returns a handle, + * the callback fnc is called to handle httpread_event events. + * The caller should do destroy on any errors or unknown events. + * + * Pass max_bytes == 0 to not read body at all (required for e.g. + * reply to HEAD request). + */ +struct httpread * httpread_create( + int sd, /* descriptor of TCP socket to read from */ + void (*cb)(struct httpread *handle, void *cookie, + enum httpread_event e), /* call on event */ + void *cookie, /* pass to callback */ + int max_bytes, /* maximum file size else abort it */ + int timeout_seconds /* 0; or total duration timeout period */ + ); + +/* httpread_hdr_type_get -- When file is ready, returns header type. + */ +enum httpread_hdr_type httpread_hdr_type_get(struct httpread *h); + + +/* httpread_uri_get -- When file is ready, uri_get returns (translated) URI + * or possibly NULL (which would be an error). + */ +char *httpread_uri_get(struct httpread *h); + +/* httpread_reply_code_get -- When reply is ready, returns reply code */ +int httpread_reply_code_get(struct httpread *h); + + +/* httpread_length_get -- When file is ready, returns file length. */ +int httpread_length_get(struct httpread *h); + +/* httpread_data_get -- When file is ready, returns file content + * with null byte appened. + * Might return NULL in some error condition. + */ +void * httpread_data_get(struct httpread *h); + +/* httpread_hdr_get -- When file is ready, returns header content + * with null byte appended. + * Might return NULL in some error condition. + */ +char * httpread_hdr_get(struct httpread *h); + +/* httpread_hdr_line_get -- When file is ready, returns pointer + * to line within header content matching the given tag + * (after the tag itself and any spaces/tabs). + * + * The tag should end with a colon for reliable matching. + * + * If not found, returns NULL; + */ +char * httpread_hdr_line_get(struct httpread *h, const char *tag); + +#endif /* HTTPREAD_H */ diff --git a/src/wps/wps.h b/src/wps/wps.h index e18adab24..c42ce1e34 100644 --- a/src/wps/wps.h +++ b/src/wps/wps.h @@ -21,6 +21,7 @@ * enum wsc_op_code - EAP-WSC OP-Code values */ enum wsc_op_code { + WSC_UPnP = 0 /* No OP Code in UPnP transport */, WSC_Start = 0x01, WSC_ACK = 0x02, WSC_NACK = 0x03, @@ -30,6 +31,7 @@ enum wsc_op_code { }; struct wps_registrar; +struct upnp_wps_device_sm; /** * struct wps_credential - WPS Credential @@ -142,7 +144,13 @@ enum wps_process_res { /** * WPS_FAILURE - Processing failed */ - WPS_FAILURE + WPS_FAILURE, + + /** + * WPS_PENDING - Processing continues, but waiting for an external + * event (e.g., UPnP message from an external Registrar) + */ + WPS_PENDING }; enum wps_process_res wps_process_msg(struct wps_data *wps, enum wsc_op_code op_code, @@ -419,6 +427,31 @@ struct wps_context { */ size_t ap_settings_len; + /** + * friendly_name - Friendly Name (required for UPnP) + */ + char *friendly_name; + + /** + * manufacturer_url - Manufacturer URL (optional for UPnP) + */ + char *manufacturer_url; + + /** + * model_description - Model Description (recommended for UPnP) + */ + char *model_description; + + /** + * model_url - Model URL (optional for UPnP) + */ + char *model_url; + + /** + * upc - Universal Product Code (optional for UPnP) + */ + char *upc; + /** * cred_cb - Callback to notify that new Credentials were received * @ctx: Higher layer context data (cb_ctx) @@ -440,6 +473,13 @@ struct wps_context { * cb_ctx: Higher layer context data for callbacks */ void *cb_ctx; + + struct upnp_wps_device_sm *wps_upnp; + + /* TODO: support multiple pending messages from UPnP PutWLANResponse */ + u8 upnp_msg_addr[ETH_ALEN]; + struct wpabuf *upnp_msg; + void *pending_session; }; @@ -455,6 +495,8 @@ int wps_registrar_button_pushed(struct wps_registrar *reg); void wps_registrar_probe_req_rx(struct wps_registrar *reg, const u8 *addr, const struct wpabuf *wps_data); int wps_registrar_update_ie(struct wps_registrar *reg); +int wps_registrar_set_selected_registrar(struct wps_registrar *reg, + const struct wpabuf *msg); unsigned int wps_pin_checksum(unsigned int pin); unsigned int wps_pin_valid(unsigned int pin); diff --git a/src/wps/wps_enrollee.c b/src/wps/wps_enrollee.c index c3be2485d..6b6bc2759 100644 --- a/src/wps/wps_enrollee.c +++ b/src/wps/wps_enrollee.c @@ -32,7 +32,13 @@ static int wps_build_mac_addr(struct wps_data *wps, struct wpabuf *msg) static int wps_build_wps_state(struct wps_data *wps, struct wpabuf *msg) { - wpa_printf(MSG_DEBUG, "WPS: * Wi-Fi Protected Setup State"); + u8 state; + if (wps->wps->ap) + state = wps->wps->wps_state; + else + state = WPS_STATE_NOT_CONFIGURED; + wpa_printf(MSG_DEBUG, "WPS: * Wi-Fi Protected Setup State (%d)", + state); wpabuf_put_be16(msg, ATTR_WPS_STATE); wpabuf_put_be16(msg, 1); wpabuf_put_u8(msg, WPS_STATE_NOT_CONFIGURED); @@ -1155,6 +1161,7 @@ enum wps_process_res wps_enrollee_process_msg(struct wps_data *wps, switch (op_code) { case WSC_MSG: + case WSC_UPnP: return wps_process_wsc_msg(wps, msg); case WSC_ACK: return wps_process_wsc_ack(wps, msg); diff --git a/src/wps/wps_i.h b/src/wps/wps_i.h index 7221af377..b32a68f4c 100644 --- a/src/wps/wps_i.h +++ b/src/wps/wps_i.h @@ -101,6 +101,8 @@ struct wps_data { * config_error - Configuration Error value to be used in NACK */ u16 config_error; + + int ext_reg; }; diff --git a/src/wps/wps_registrar.c b/src/wps/wps_registrar.c index 8ef982bd4..249068211 100644 --- a/src/wps/wps_registrar.c +++ b/src/wps/wps_registrar.c @@ -21,6 +21,7 @@ #include "eloop.h" #include "wps_i.h" #include "wps_dev_attr.h" +#include "wps_upnp.h" struct wps_uuid_pin { @@ -95,11 +96,15 @@ struct wps_registrar { int skip_cred_build; struct wpabuf *extra_cred; int disable_auto_conf; + int sel_reg_dev_password_id_override; + int sel_reg_config_methods_override; }; static int wps_set_ie(struct wps_registrar *reg); static void wps_registrar_pbc_timeout(void *eloop_ctx, void *timeout_ctx); +static void wps_registrar_set_selected_timeout(void *eloop_ctx, + void *timeout_ctx); static void wps_registrar_add_pbc_session(struct wps_registrar *reg, @@ -243,6 +248,8 @@ static int wps_build_sel_reg_dev_password_id(struct wps_registrar *reg, u16 id = reg->pbc ? DEV_PW_PUSHBUTTON : DEV_PW_DEFAULT; if (!reg->selected_registrar) return 0; + if (reg->sel_reg_dev_password_id_override >= 0) + id = reg->sel_reg_dev_password_id_override; wpa_printf(MSG_DEBUG, "WPS: * Device Password ID (%d)", id); wpabuf_put_be16(msg, ATTR_DEV_PASSWORD_ID); wpabuf_put_be16(msg, 2); @@ -260,6 +267,8 @@ static int wps_build_sel_reg_config_methods(struct wps_registrar *reg, methods = reg->wps->config_methods & ~WPS_CONFIG_PUSHBUTTON; if (reg->pbc) methods |= WPS_CONFIG_PUSHBUTTON; + if (reg->sel_reg_config_methods_override >= 0) + methods = reg->sel_reg_config_methods_override; wpa_printf(MSG_DEBUG, "WPS: * Selected Registrar Config Methods (%x)", methods); wpabuf_put_be16(msg, ATTR_SELECTED_REGISTRAR_CONFIG_METHODS); @@ -340,6 +349,7 @@ wps_registrar_init(struct wps_context *wps, } } reg->disable_auto_conf = cfg->disable_auto_conf; + reg->sel_reg_dev_password_id_override = -1; if (wps_set_ie(reg)) { wps_registrar_deinit(reg); @@ -359,6 +369,7 @@ void wps_registrar_deinit(struct wps_registrar *reg) if (reg == NULL) return; eloop_cancel_timeout(wps_registrar_pbc_timeout, reg, NULL); + eloop_cancel_timeout(wps_registrar_set_selected_timeout, reg, NULL); wps_free_pins(reg->pins); wps_free_pbc_sessions(reg->pbc_sessions); wpabuf_free(reg->extra_cred); @@ -1311,6 +1322,22 @@ struct wpabuf * wps_registrar_get_msg(struct wps_data *wps, { struct wpabuf *msg; +#ifdef CONFIG_WPS_UPNP + if (wps->wps->wps_upnp && wps->wps->upnp_msg) { + wpa_printf(MSG_DEBUG, "WPS: Use pending message from UPnP"); + msg = wps->wps->upnp_msg; + wps->wps->upnp_msg = NULL; + *op_code = WSC_MSG; /* FIX: ack/nack */ + wps->ext_reg = 1; + return msg; + } + if (wps->ext_reg) { + wpa_printf(MSG_DEBUG, "WPS: Using external Registrar, but no " + "pending message available"); + return NULL; + } +#endif /* CONFIG_WPS_UPNP */ + switch (wps->state) { case SEND_M2: if (wps_get_dev_password(wps) < 0) @@ -1927,6 +1954,17 @@ static enum wps_process_res wps_process_wsc_msg(struct wps_data *wps, switch (*attr.msg_type) { case WPS_M1: +#ifdef CONFIG_WPS_UPNP + if (wps->wps->wps_upnp && attr.mac_addr) { + /* Remove old pending messages when starting new run */ + wpabuf_free(wps->wps->upnp_msg); + wps->wps->upnp_msg = NULL; + + upnp_wps_device_send_wlan_event( + wps->wps->wps_upnp, attr.mac_addr, + UPNP_WPS_WLANEVENT_TYPE_EAP, msg); + } +#endif /* CONFIG_WPS_UPNP */ ret = wps_process_m1(wps, &attr); break; case WPS_M3: @@ -1988,6 +2026,17 @@ static enum wps_process_res wps_process_wsc_ack(struct wps_data *wps, return WPS_FAILURE; } +#ifdef CONFIG_WPS_UPNP + if (wps->wps->wps_upnp && wps->ext_reg && wps->state == RECV_M2D_ACK && + upnp_wps_subscribers(wps->wps->wps_upnp)) { + if (wps->wps->upnp_msg) + return WPS_CONTINUE; + wpa_printf(MSG_DEBUG, "WPS: Wait for response from an " + "external Registrar"); + return WPS_PENDING; + } +#endif /* CONFIG_WPS_UPNP */ + if (attr.registrar_nonce == NULL || os_memcmp(wps->nonce_r, attr.registrar_nonce, WPS_NONCE_LEN != 0)) { @@ -2002,8 +2051,16 @@ static enum wps_process_res wps_process_wsc_ack(struct wps_data *wps, } if (wps->state == RECV_M2D_ACK) { - /* TODO: support for multiple registrars and sending of - * multiple M2/M2D messages */ +#ifdef CONFIG_WPS_UPNP + if (wps->wps->wps_upnp && + upnp_wps_subscribers(wps->wps->wps_upnp)) { + if (wps->wps->upnp_msg) + return WPS_CONTINUE; + wpa_printf(MSG_DEBUG, "WPS: Wait for response from an " + "external Registrar"); + return WPS_PENDING; + } +#endif /* CONFIG_WPS_UPNP */ wpa_printf(MSG_DEBUG, "WPS: No more registrars available - " "terminate negotiation"); @@ -2044,6 +2101,14 @@ static enum wps_process_res wps_process_wsc_nack(struct wps_data *wps, return WPS_FAILURE; } +#ifdef CONFIG_WPS_UPNP + if (wps->wps->wps_upnp && wps->ext_reg) { + wpa_printf(MSG_DEBUG, "WPS: Negotiation using external " + "Registrar terminated by the Enrollee"); + return WPS_FAILURE; + } +#endif /* CONFIG_WPS_UPNP */ + if (attr.registrar_nonce == NULL || os_memcmp(wps->nonce_r, attr.registrar_nonce, WPS_NONCE_LEN != 0)) { @@ -2094,7 +2159,8 @@ static enum wps_process_res wps_process_wsc_done(struct wps_data *wps, wpa_printf(MSG_DEBUG, "WPS: Received WSC_Done"); - if (wps->state != RECV_DONE) { + if (wps->state != RECV_DONE && + (!wps->wps->wps_upnp || !wps->ext_reg)) { wpa_printf(MSG_DEBUG, "WPS: Unexpected state (%d) for " "receiving WSC_Done", wps->state); return WPS_FAILURE; @@ -2120,6 +2186,14 @@ static enum wps_process_res wps_process_wsc_done(struct wps_data *wps, return WPS_FAILURE; } +#ifdef CONFIG_WPS_UPNP + if (wps->wps->wps_upnp && wps->ext_reg) { + wpa_printf(MSG_DEBUG, "WPS: Negotiation using external " + "Registrar completed successfully"); + return WPS_DONE; + } +#endif /* CONFIG_WPS_UPNP */ + if (attr.registrar_nonce == NULL || os_memcmp(wps->nonce_r, attr.registrar_nonce, WPS_NONCE_LEN != 0)) { @@ -2202,6 +2276,30 @@ enum wps_process_res wps_registrar_process_msg(struct wps_data *wps, "op_code=%d)", (unsigned long) wpabuf_len(msg), op_code); +#ifdef CONFIG_WPS_UPNP + if (wps->wps->wps_upnp && wps->ext_reg && wps->wps->upnp_msg == NULL && + (op_code == WSC_MSG || op_code == WSC_Done)) { + struct wps_parse_attr attr; + int type; + if (wps_parse_msg(msg, &attr) < 0 || attr.msg_type == NULL) + type = -1; + else + type = *attr.msg_type; + wpa_printf(MSG_DEBUG, "WPS: Sending received message (type %d)" + " to external Registrar for processing", type); + upnp_wps_device_send_wlan_event(wps->wps->wps_upnp, + wps->mac_addr_e, + UPNP_WPS_WLANEVENT_TYPE_EAP, + msg); + if (op_code == WSC_MSG) + return WPS_PENDING; + } else if (wps->wps->wps_upnp && wps->ext_reg && op_code == WSC_MSG) { + wpa_printf(MSG_DEBUG, "WPS: Skip internal processing - using " + "external Registrar"); + return WPS_CONTINUE; + } +#endif /* CONFIG_WPS_UPNP */ + switch (op_code) { case WSC_MSG: return wps_process_wsc_msg(wps, msg); @@ -2227,3 +2325,68 @@ int wps_registrar_update_ie(struct wps_registrar *reg) { return wps_set_ie(reg); } + + +static void wps_registrar_set_selected_timeout(void *eloop_ctx, + void *timeout_ctx) +{ + struct wps_registrar *reg = eloop_ctx; + + wpa_printf(MSG_DEBUG, "WPS: SetSelectedRegistrar timed out - " + "unselect Registrar"); + reg->selected_registrar = 0; + reg->pbc = 0; + reg->sel_reg_dev_password_id_override = -1; + reg->sel_reg_config_methods_override = -1; + wps_set_ie(reg); +} + + +/** + * wps_registrar_set_selected_registrar - Notification of SetSelectedRegistrar + * @reg: Registrar data from wps_registrar_init() + * @msg: Received message from SetSelectedRegistrar + * @msg_len: Length of msg in octets + * Returns: 0 on success, -1 on failure + * + * This function is called when an AP receives a SetSelectedRegistrar UPnP + * message. + */ +int wps_registrar_set_selected_registrar(struct wps_registrar *reg, + const struct wpabuf *msg) +{ + struct wps_parse_attr attr; + + wpa_hexdump_buf(MSG_MSGDUMP, "WPS: SetSelectedRegistrar attributes", + msg); + + if (wps_parse_msg(msg, &attr) < 0 || + attr.version == NULL || *attr.version != WPS_VERSION) { + wpa_printf(MSG_DEBUG, "WPS: Unsupported SetSelectedRegistrar " + "version 0x%x", attr.version ? *attr.version : 0); + return -1; + } + + if (attr.selected_registrar == NULL || + *attr.selected_registrar == 0) { + wpa_printf(MSG_DEBUG, "WPS: SetSelectedRegistrar: Disable " + "Selected Registrar"); + eloop_cancel_timeout(wps_registrar_set_selected_timeout, reg, + NULL); + wps_registrar_set_selected_timeout(reg, NULL); + return 0; + } + + reg->selected_registrar = 1; + reg->sel_reg_dev_password_id_override = attr.dev_password_id ? + WPA_GET_BE16(attr.dev_password_id) : DEV_PW_DEFAULT; + reg->sel_reg_config_methods_override = attr.sel_reg_config_methods ? + WPA_GET_BE16(attr.sel_reg_config_methods) : -1; + wps_set_ie(reg); + + eloop_cancel_timeout(wps_registrar_set_selected_timeout, reg, NULL); + eloop_register_timeout(WPS_PBC_WALK_TIME, 0, + wps_registrar_set_selected_timeout, + reg, NULL); + return 0; +} diff --git a/src/wps/wps_upnp.c b/src/wps/wps_upnp.c new file mode 100644 index 000000000..bebecc2a7 --- /dev/null +++ b/src/wps/wps_upnp.c @@ -0,0 +1,1056 @@ +/* + * UPnP WPS Device + * Copyright (c) 2000-2003 Intel Corporation + * Copyright (c) 2006-2007 Sony Corporation + * Copyright (c) 2008-2009 Atheros Communications + * Copyright (c) 2009, Jouni Malinen + * + * See below for more details on licensing and code history. + */ + +/* + * This has been greatly stripped down from the original file + * (upnp_wps_device.c) by Ted Merrill, Atheros Communications + * in order to eliminate use of the bulky libupnp library etc. + * + * History: + * upnp_wps_device.c is/was a shim layer between wps_opt_upnp.c and + * the libupnp library. + * The layering (by Sony) was well done; only a very minor modification + * to API of upnp_wps_device.c was required. + * libupnp was found to be undesirable because: + * -- It consumed too much code and data space + * -- It uses multiple threads, making debugging more difficult + * and possibly reducing reliability. + * -- It uses static variables and only supports one instance. + * The shim and libupnp are here replaced by special code written + * specifically for the needs of hostapd. + * Various shortcuts can and are taken to keep the code size small. + * Generally, execution time is not as crucial. + * + * BUGS: + * -- UPnP requires that we be able to resolve domain names. + * While uncommon, if we have to do it then it will stall the entire + * hostapd program, which is bad. + * This is because we use the standard linux getaddrinfo() function + * which is syncronous. + * An asyncronous solution would be to use the free "ares" library. + * -- Does not have a robust output buffering scheme. Uses a single + * fixed size output buffer per TCP/HTTP connection, with possible (although + * unlikely) possibility of overflow and likely excessive use of RAM. + * A better solution would be to write the HTTP output as a buffered stream, + * using chunking: (handle header specially, then) generate data with + * a printf-like function into a buffer, catching buffer full condition, + * then send it out surrounded by http chunking. + * -- There is some code that could be separated out into the common + * library to be shared with wpa_supplicant. + * -- Needs renaming with module prefix to avoid polluting the debugger + * namespace and causing possible collisions with other static fncs + * and structure declarations when using the debugger. + * -- Just what should be in the first event message sent after subscription + * for the WLANEvent field? If i pass it empty, Vista replies with OK + * but apparently barfs on the message. + * -- The http error code generation is pretty bogus, hopefully noone cares. + * + * Author: Ted Merrill, Atheros Communications, based upon earlier work + * as explained above and below. + * + * Copyright: + * Copyright 2008 Atheros Communications. + * + * The original header (of upnp_wps_device.c) reads: + * + * Copyright (c) 2006-2007 Sony Corporation. All Rights Reserved. + * + * File Name: upnp_wps_device.c + * Description: EAP-WPS UPnP device source + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Sony Corporation nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * Portions from Intel libupnp files, e.g. genlib/net/http/httpreadwrite.c + * typical header: + * + * Copyright (c) 2000-2003 Intel Corporation + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither name of Intel Corporation nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL INTEL OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +/* + * Overview of WPS over UPnP: + * + * UPnP is a protocol that allows devices to discover each other and control + * each other. In UPnP terminology, a device is either a "device" (a server + * that provides information about itself and allows itself to be controlled) + * or a "control point" (a client that controls "devices") or possibly both. + * This file implements a UPnP "device". + * + * For us, we use mostly basic UPnP discovery, but the control part of interest + * is WPS carried via UPnP messages. There is quite a bit of basic UPnP + * discovery to do before we can get to WPS, however. + * + * UPnP discovery begins with "devices" send out multicast UDP packets to a + * certain fixed multicast IP address and port, and "control points" sending + * out other such UDP packets. + * + * The packets sent by devices are NOTIFY packets (not to be confused with TCP + * NOTIFY packets that are used later) and those sent by control points are + * M-SEARCH packets. These packets contain a simple HTTP style header. The + * packets are sent redundantly to get around packet loss. Devices respond to + * M-SEARCH packets with HTTP-like UDP packets containing HTTP/1.1 200 OK + * messages, which give similar information as the UDP NOTIFY packets. + * + * The above UDP packets advertise the (arbitrary) TCP ports that the + * respective parties will listen to. The control point can then do a HTTP + * SUBSCRIBE (something like an HTTP PUT) after which the device can do a + * separate HTTP NOTIFY (also like an HTTP PUT) to do event messaging. + * + * The control point will also do HTTP GET of the "device file" listed in the + * original UDP information from the device (see UPNP_WPS_DEVICE_XML_FILE + * data), and based on this will do additional GETs... HTTP POSTs are done to + * cause an action. + * + * Beyond some basic information in HTTP headers, additional information is in + * the HTTP bodies, in a format set by the SOAP and XML standards, a markup + * language related to HTML used for web pages. This language is intended to + * provide the ultimate in self-documentation by providing a universal + * namespace based on pseudo-URLs called URIs. Note that although a URI looks + * like a URL (a web address), they are never accessed as such but are used + * only as identifiers. + * + * The POST of a GetDeviceInfo gets information similar to what might be + * obtained from a probe request or response on Wi-Fi. WPS messages M1-M8 + * are passed via a POST of a PutMessage; the M1-M8 WPS messages are converted + * to a bin64 ascii representation for encapsulation. When proxying messages, + * WLANEvent and PutWLANResponse are used. + * + * This of course glosses over a lot of details. + */ + +#include "includes.h" + +#include +#include +#include +#include + +#include "common.h" +#include "uuid.h" +#include "base64.h" +#include "wps.h" +#include "wps_i.h" +#include "wps_upnp.h" +#include "wps_upnp_i.h" + + +/* + * UPnP allows a client ("control point") to send a server like us ("device") + * a domain name for registration, and we are supposed to resolve it. This is + * bad because, using the standard Linux library, we will stall the entire + * hostapd waiting for resolution. + * + * The "correct" solution would be to use an event driven library for domain + * name resolution such as "ares". However, this would increase code size + * further. Since it is unlikely that we'll actually see such domain names, we + * can just refuse to accept them. + */ +#define NO_DOMAIN_NAME_RESOLUTION 1 /* 1 to allow only dotted ip addresses */ + + +/* + * UPnP does not scale well. If we were in a room with thousands of control + * points then potentially we could be expected to handle subscriptions for + * each of them, which would exhaust our memory. So we must set a limit. In + * practice we are unlikely to see more than one or two. + */ +#define MAX_SUBSCRIPTIONS 4 /* how many subscribing clients we handle */ +#define MAX_ADDR_PER_SUBSCRIPTION 8 + + +/* Write the current date/time per RFC */ +void format_date(struct wpabuf *buf) +{ + const char *weekday_str = "Sun\0Mon\0Tue\0Wed\0Thu\0Fri\0Sat"; + const char *month_str = "Jan\0Feb\0Mar\0Apr\0May\0Jun\0" + "Jul\0Aug\0Sep\0Oct\0Nov\0Dec"; + struct tm *date; + time_t t; + + t = time(NULL); + date = gmtime(&t); + wpabuf_printf(buf, "%s, %02d %s %d %02d:%02d:%02d GMT", + &weekday_str[date->tm_wday * 4], date->tm_mday, + &month_str[date->tm_mon * 4], date->tm_year + 1900, + date->tm_hour, date->tm_min, date->tm_sec); +} + + +/*************************************************************************** + * UUIDs (unique identifiers) + * + * These are supposed to be unique in all the world. + * Sometimes permanent ones are used, sometimes temporary ones + * based on random numbers... there are different rules for valid content + * of different types. + * Each uuid is 16 bytes long. + **************************************************************************/ + +/* uuid_make -- construct a random UUID + * The UPnP documents don't seem to offer any guidelines as to which method to + * use for constructing UUIDs for subscriptions. Presumably any method from + * rfc4122 is good enough; I've chosen random number method. + */ +static void uuid_make(u8 uuid[UUID_LEN]) +{ + os_get_random(uuid, UUID_LEN); + + /* Replace certain bits as specified in rfc4122 or X.667 */ + uuid[6] &= 0x0f; uuid[6] |= (4 << 4); /* version 4 == random gen */ + uuid[8] &= 0x3f; uuid[8] |= 0x80; +} + + +/* + * Subscriber address handling. + * Since a subscriber may have an arbitrary number of addresses, we have to + * add a bunch of code to handle them. + * + * Addresses are passed in text, and MAY be domain names instead of the (usual + * and expected) dotted IP addresses. Resolving domain names consumes a lot of + * resources. Worse, we are currently using the standard Linux getaddrinfo() + * which will block the entire program until complete or timeout! The proper + * solution would be to use the "ares" library or similar with more state + * machine steps etc. or just disable domain name resolution by setting + * NO_DOMAIN_NAME_RESOLUTION to 1 at top of this file. + */ + +/* subscr_addr_delete -- delete single unlinked subscriber address + * (be sure to unlink first if need be) + */ +static void subscr_addr_delete(struct subscr_addr *a) +{ + /* + * Note: do NOT free domain_and_port or path because they point to + * memory within the allocation of "a". + */ + os_free(a); +} + + +/* subscr_addr_unlink -- unlink subscriber address from linked list */ +static void subscr_addr_unlink(struct subscription *s, struct subscr_addr *a) +{ + struct subscr_addr **listp = &s->addr_list; + s->n_addr--; + a->next->prev = a->prev; + a->prev->next = a->next; + if (*listp == a) { + if (a == a->next) { + /* last in queue */ + *listp = NULL; + assert(s->n_addr == 0); + } else { + *listp = a->next; + } + } +} + + +/* subscr_addr_free_all -- unlink and delete list of subscriber addresses. */ +static void subscr_addr_free_all(struct subscription *s) +{ + struct subscr_addr **listp = &s->addr_list; + struct subscr_addr *a; + while ((a = *listp) != NULL) { + subscr_addr_unlink(s, a); + subscr_addr_delete(a); + } +} + + +/* subscr_addr_link -- add subscriber address to list of addresses */ +static void subscr_addr_link(struct subscription *s, struct subscr_addr *a) +{ + struct subscr_addr **listp = &s->addr_list; + s->n_addr++; + if (*listp == NULL) { + *listp = a->next = a->prev = a; + } else { + a->next = *listp; + a->prev = (*listp)->prev; + a->prev->next = a; + a->next->prev = a; + } +} + + +/* subscr_addr_add_url -- add address(es) for one url to subscription */ +static void subscr_addr_add_url(struct subscription *s, const char *url) +{ + int alloc_len; + char *scratch_mem = NULL; + char *mem; + char *domain_and_port; + char *delim; + char *path; + char *domain; + int port = 80; /* port to send to (default is port 80) */ + struct addrinfo hints; + struct addrinfo *result = NULL; + struct addrinfo *rp; + int rerr; + struct subscr_addr *a = NULL; + + /* url MUST begin with http: */ + if (os_strncasecmp(url, "http://", 7)) + goto fail; + url += 7; + + /* allocate memory for the extra stuff we need */ + alloc_len = (2 * (os_strlen(url) + 1)); + scratch_mem = os_zalloc(alloc_len); + if (scratch_mem == NULL) + goto fail; + mem = scratch_mem; + strcpy(mem, url); + domain_and_port = mem; + mem += 1 + os_strlen(mem); + delim = os_strchr(domain_and_port, '/'); + if (delim) { + *delim++ = 0; /* null terminate domain and port */ + path = delim; + } else { + path = domain_and_port + os_strlen(domain_and_port); + } + domain = mem; + strcpy(domain, domain_and_port); + delim = strchr(domain, ':'); + if (delim) { + *delim++ = 0; /* null terminate domain */ + if (isdigit(*delim)) + port = atol(delim); + } + + /* + * getaddrinfo does the right thing with dotted decimal notations, or + * will resolve domain names. Resolving domain names will unfortunately + * hang the entire program until it is resolved or it times out + * internal to getaddrinfo; fortunately we think that the use of actual + * domain names (vs. dotted decimal notations) should be uncommon. + */ + os_memset(&hints, 0, sizeof(struct addrinfo)); + hints.ai_family = AF_INET; /* IPv4 */ + hints.ai_socktype = SOCK_STREAM; +#if NO_DOMAIN_NAME_RESOLUTION + /* Suppress domain name resolutions that would halt + * the program for periods of time + */ + hints.ai_flags = AI_NUMERICHOST; +#else + /* Allow domain name resolution. */ + hints.ai_flags = 0; +#endif + hints.ai_protocol = 0; /* Any protocol? */ + rerr = getaddrinfo(domain, NULL /* fill in port ourselves */, + &hints, &result); + if (rerr) { + wpa_printf(MSG_INFO, "WPS UPnP: Resolve error %d (%s) on: %s", + rerr, gai_strerror(rerr), domain); + goto fail; + } + for (rp = result; rp; rp = rp->ai_next) { + /* Limit no. of address to avoid denial of service attack */ + if (s->n_addr >= MAX_ADDR_PER_SUBSCRIPTION) { + wpa_printf(MSG_INFO, "WPS UPnP: subscr_addr_add_url: " + "Ignoring excessive addresses"); + break; + } + + a = os_zalloc(sizeof(*a) + alloc_len); + if (a == NULL) + continue; + a->s = s; + mem = (void *) (a + 1); + a->domain_and_port = mem; + strcpy(mem, domain_and_port); + mem += 1 + strlen(mem); + a->path = mem; + if (path[0] != '/') + *mem++ = '/'; + strcpy(mem, path); + mem += 1 + strlen(mem); + os_memcpy(&a->saddr, rp->ai_addr, sizeof(a->saddr)); + a->saddr.sin_port = htons(port); + + subscr_addr_link(s, a); + a = NULL; /* don't free it below */ + } + +fail: + if (result) + freeaddrinfo(result); + os_free(scratch_mem); + os_free(a); +} + + +/* subscr_addr_list_create -- create list from urls in string. + * Each url is enclosed by angle brackets. + */ +static void subscr_addr_list_create(struct subscription *s, + const char *url_list) +{ + char *end; + for (;;) { + while (*url_list == ' ' || *url_list == '\t') + url_list++; + if (*url_list != '<') + break; + url_list++; + end = os_strchr(url_list, '>'); + if (end == NULL) + break; + *end++ = 0; + subscr_addr_add_url(s, url_list); + url_list = end; + } +} + + +int send_wpabuf(int fd, struct wpabuf *buf) +{ + wpa_printf(MSG_DEBUG, "WPS UPnP: %lu byte response", + (unsigned long) wpabuf_len(buf)); + errno = 0; + if (write(fd, wpabuf_head(buf), wpabuf_len(buf)) != + (int) wpabuf_len(buf)) { + wpa_printf(MSG_ERROR, "WPS UPnP: Failed to send buffer: " + "errno=%d (%s)", + errno, strerror(errno)); + return -1; + } + + return 0; +} + + +static void wpabuf_put_property(struct wpabuf *buf, const char *name, + const char *value) +{ + wpabuf_put_str(buf, ""); + wpabuf_printf(buf, "<%s>", name); + if (value) + wpabuf_put_str(buf, value); + wpabuf_printf(buf, "", name); + wpabuf_put_str(buf, "\n"); +} + + +/** + * upnp_wps_device_send_event - Queue event messages for subscribers + * @sm: WPS UPnP state machine from upnp_wps_device_init() + * + * This function queues the last WLANEvent to be sent for all currently + * subscribed UPnP control points. sm->wlanevent must have been set with the + * encoded data before calling this function. + */ +static void upnp_wps_device_send_event(struct upnp_wps_device_sm *sm) +{ + /* Enqueue event message for all subscribers */ + struct wpabuf *buf; /* holds event message */ + int buf_size = 0; + struct subscription *s; + /* Actually, utf-8 is the default, but it doesn't hurt to specify it */ + const char *format_head = + "\n" + "\n"; + const char *format_tail = "\n"; + + if (sm->subscriptions == NULL) { + /* optimize */ + return; + } + + /* Determine buffer size needed first */ + buf_size += os_strlen(format_head); + buf_size += 50 + 2 * os_strlen("WLANEvent"); + if (sm->wlanevent) + buf_size += os_strlen(sm->wlanevent); + buf_size += os_strlen(format_tail); + + buf = wpabuf_alloc(buf_size); + if (buf == NULL) + return; + wpabuf_put_str(buf, format_head); + wpabuf_put_property(buf, "WLANEvent", sm->wlanevent); + wpabuf_put_str(buf, format_tail); + + wpa_printf(MSG_MSGDUMP, "WPS UPnP: WLANEvent message:\n%s", + (char *) wpabuf_head(buf)); + + s = sm->subscriptions; + do { + if (event_add(s, buf)) { + struct subscription *s_old = s; + wpa_printf(MSG_INFO, "WPS UPnP: Dropping " + "subscriber due to event backlog"); + s = s_old->next; + subscription_unlink(s_old); + subscription_destroy(s_old); + } else { + s = s->next; + } + } while (s != sm->subscriptions); + + wpabuf_free(buf); +} + + +/* + * Event subscription (subscriber machines register with us to receive event + * messages). + * This is the result of an incoming HTTP over TCP SUBSCRIBE request. + */ + +/* subscription_unlink -- remove from the active list */ +void subscription_unlink(struct subscription *s) +{ + struct upnp_wps_device_sm *sm = s->sm; + + if (s->next == s) { + /* only one? */ + sm->subscriptions = NULL; + } else { + if (sm->subscriptions == s) { + sm->subscriptions = s->next; + } + s->next->prev = s->prev; + s->prev->next = s->next; + } + sm->n_subscriptions--; +} + + +/* subscription_link_to_end -- link to end of active list + * (should have high expiry time!) + */ +static void subscription_link_to_end(struct subscription *s) +{ + struct upnp_wps_device_sm *sm = s->sm; + + if (sm->subscriptions) { + s->next = sm->subscriptions; + s->prev = s->next->prev; + s->prev->next = s; + s->next->prev = s; + } else { + sm->subscriptions = s->next = s->prev = s; + } + sm->n_subscriptions++; +} + + +/* subscription_destroy -- destroy an unlinked subscription + * Be sure to unlink first if necessary. + */ +void subscription_destroy(struct subscription *s) +{ + wpa_printf(MSG_DEBUG, "WPS UPnP: Destroy subscription %p", s); + if (s->addr_list) + subscr_addr_free_all(s); + event_delete_all(s); + os_free(s); +} + + +/* subscription_list_age -- remove expired subscriptions */ +static void subscription_list_age(struct upnp_wps_device_sm *sm, time_t now) +{ + struct subscription *s; + while ((s = sm->subscriptions) != NULL && s->timeout_time < now) { + wpa_printf(MSG_DEBUG, "WPS UPnP: Removing aged subscription"); + subscription_unlink(s); + subscription_destroy(s); + } +} + + +/* subscription_find -- return existing subscription matching uuid, if any + * returns NULL if not found + */ +struct subscription * subscription_find(struct upnp_wps_device_sm *sm, + const u8 uuid[UUID_LEN]) +{ + struct subscription *s0 = sm->subscriptions; + struct subscription *s = s0; + + if (s0 == NULL) + return NULL; + do { + if (os_memcmp(s->uuid, uuid, UUID_LEN) == 0) + return s; /* Found match */ + s = s->next; + } while (s != s0); + + return NULL; +} + + +/* subscription_first_event -- send format/queue event that is automatically + * sent on a new subscription. + */ +static int subscription_first_event(struct subscription *s) +{ + /* + * Actually, utf-8 is the default, but it doesn't hurt to specify it. + * + * APStatus is apparently a bit set, + * 0x1 = configuration change (but is always set?) + * 0x10 = ap is locked + * + * Per UPnP spec, we send out the last value of each variable, even + * for WLANEvent, whatever it was. + */ + char *wlan_event; + struct wpabuf *buf; + int ap_status = 1; /* TODO: add 0x10 if access point is locked */ + const char *head = + "\n" + "\n"; + const char *tail = "\n"; + char txt[10]; + + wlan_event = s->sm->wlanevent; + if (wlan_event == NULL || *wlan_event == '\0') { + wpa_printf(MSG_DEBUG, "WPS UPnP: WLANEvent not known for " + "initial event message"); + wlan_event = ""; + } + buf = wpabuf_alloc(500 + os_strlen(wlan_event)); + if (buf == NULL) + return 1; + + wpabuf_put_str(buf, head); + wpabuf_put_property(buf, "STAStatus", "1"); + os_snprintf(txt, sizeof(txt), "%d", ap_status); + wpabuf_put_property(buf, "APStatus", txt); + if (*wlan_event) + wpabuf_put_property(buf, "WLANEvent", wlan_event); + wpabuf_put_str(buf, tail); + + if (event_add(s, buf)) { + wpabuf_free(buf); + return 1; + } + wpabuf_free(buf); + + return 0; +} + + +/** + * subscription_start - Rremember a UPnP control point to send events to. + * @sm: WPS UPnP state machine from upnp_wps_device_init() + * @callback_urls: malloc' mem given to the subscription + * Returns: %NULL on error, or pointer to new subscription structure. + */ +struct subscription * subscription_start(struct upnp_wps_device_sm *sm, + char *callback_urls) +{ + struct subscription *s = NULL; + time_t now = time(NULL); + time_t expire = now + UPNP_SUBSCRIBE_SEC; + + /* Get rid of expired subscriptions so we have room */ + subscription_list_age(sm, now); + + /* If too many subscriptions, remove oldest */ + if (sm->n_subscriptions >= MAX_SUBSCRIPTIONS) { + struct subscription *s = sm->subscriptions; + wpa_printf(MSG_INFO, "WPS UPnP: Too many subscriptions, " + "trashing oldest"); + subscription_unlink(s); + subscription_destroy(s); + } + + s = os_zalloc(sizeof(*s)); + if (s == NULL) + return NULL; + + s->sm = sm; + s->timeout_time = expire; + uuid_make(s->uuid); + subscr_addr_list_create(s, callback_urls); + /* Add to end of list, since it has the highest expiration time */ + subscription_link_to_end(s); + /* Queue up immediate event message (our last event) + * as required by UPnP spec. + */ + if (subscription_first_event(s)) { + wpa_printf(MSG_INFO, "WPS UPnP: Dropping subscriber due to " + "event backlog"); + subscription_unlink(s); + subscription_destroy(s); + return NULL; + } + wpa_printf(MSG_DEBUG, "WPS UPnP: Subscription %p started with %s", + s, callback_urls); + os_free(callback_urls); + /* Schedule sending this */ + event_send_all_later(sm); + return s; +} + + +/* subscription_renew -- find subscription and reset timeout */ +struct subscription * subscription_renew(struct upnp_wps_device_sm *sm, + const u8 uuid[UUID_LEN]) +{ + time_t now = time(NULL); + time_t expire = now + UPNP_SUBSCRIBE_SEC; + struct subscription *s = subscription_find(sm, uuid); + if (s == NULL) + return NULL; + wpa_printf(MSG_DEBUG, "WPS UPnP: Subscription renewed"); + subscription_unlink(s); + s->timeout_time = expire; + /* add back to end of list, since it now has highest expiry */ + subscription_link_to_end(s); + return s; +} + + +/** + * upnp_wps_device_send_wlan_event - Event notification + * @sm: WPS UPnP state machine from upnp_wps_device_init() + * @from_mac_addr: Source (Enrollee) MAC address for the event + * @ev_type: Event type + * @msg: Event data + * Returns: 0 on success, -1 on failure + * + * Tell external Registrars (UPnP control points) that something happened. In + * particular, events include WPS messages from clients that are proxied to + * external Registrars. + */ +int upnp_wps_device_send_wlan_event(struct upnp_wps_device_sm *sm, + const u8 from_mac_addr[ETH_ALEN], + enum upnp_wps_wlanevent_type ev_type, + const struct wpabuf *msg) +{ + int ret = -1; + char type[2]; + const u8 *mac = from_mac_addr; + char mac_text[18]; + u8 *raw = NULL; + size_t raw_len; + char *val; + size_t val_len; + int pos = 0; + + if (!sm) + goto fail; + + os_snprintf(type, sizeof(type), "%1u", ev_type); + + raw_len = 1 + 17 + (msg ? wpabuf_len(msg) : 0); + raw = os_zalloc(raw_len); + if (!raw) + goto fail; + + *(raw + pos) = (u8) ev_type; + pos += 1; + os_snprintf(mac_text, sizeof(mac_text), MACSTR, MAC2STR(mac)); + wpa_printf(MSG_DEBUG, "WPS UPnP: Proxying WLANEvent from %s", + mac_text); + os_memcpy(raw + pos, mac_text, 17); + pos += 17; + if (msg) { + os_memcpy(raw + pos, wpabuf_head(msg), wpabuf_len(msg)); + pos += wpabuf_len(msg); + } + raw_len = pos; + + val = (char *) base64_encode(raw, raw_len, &val_len); + if (val == NULL) + goto fail; + + os_free(sm->wlanevent); + sm->wlanevent = val; + upnp_wps_device_send_event(sm); + + ret = 0; + +fail: + os_free(raw); + + return ret; +} + + +/** + * get_netif_info - Get hw and IP addresses for network device + * @net_if: Selected network interface name + * @ip_addr: Buffer for returning IP address in network byte order + * @ip_addr_text: Buffer for returning a pointer to allocated IP address text + * @mac: Buffer for returning MAC address + * @mac_addr_text: Buffer for returning allocated MAC address text + * Returns: 0 on success, -1 on failure + */ +static int get_netif_info(const char *net_if, unsigned *ip_addr, + char **ip_addr_text, u8 mac[ETH_ALEN], + char **mac_addr_text) +{ + struct ifreq req; + int sock = -1; + struct sockaddr_in *addr; + struct in_addr in_addr; + + *ip_addr_text = os_zalloc(16); + *mac_addr_text = os_zalloc(18); + if (*ip_addr_text == NULL || *mac_addr_text == NULL) + goto fail; + + if ((sock = socket(AF_INET, SOCK_DGRAM, 0)) < 0) + goto fail; + + os_strncpy(req.ifr_name, net_if, sizeof(req.ifr_name)); + if (ioctl(sock, SIOCGIFADDR, &req) < 0) { + wpa_printf(MSG_ERROR, "WPS UPnP: SIOCGIFADDR failed: %d (%s)", + errno, strerror(errno)); + goto fail; + } + addr = (void *) &req.ifr_addr; + *ip_addr = addr->sin_addr.s_addr; + in_addr.s_addr = *ip_addr; + os_snprintf(*ip_addr_text, 16, "%s", inet_ntoa(in_addr)); + + os_strncpy(req.ifr_name, net_if, sizeof(req.ifr_name)); + if (ioctl(sock, SIOCGIFHWADDR, &req) < 0) { + wpa_printf(MSG_ERROR, "WPS UPnP: SIOCGIFHWADDR failed: " + "%d (%s)", errno, strerror(errno)); + goto fail; + } + os_memcpy(mac, req.ifr_addr.sa_data, 6); + os_snprintf(*mac_addr_text, 18, MACSTR, MAC2STR(req.ifr_addr.sa_data)); + + close(sock); + return 0; + +fail: + if (sock >= 0) + close(sock); + os_free(*ip_addr_text); + *ip_addr_text = NULL; + os_free(*mac_addr_text); + *mac_addr_text = NULL; + return -1; +} + + +/** + * upnp_wps_device_stop - Stop WPS UPnP operations on an interface + * @sm: WPS UPnP state machine from upnp_wps_device_init() + */ +void upnp_wps_device_stop(struct upnp_wps_device_sm *sm) +{ + if (!sm || !sm->started) + return; + + wpa_printf(MSG_DEBUG, "WPS UPnP: Stop device"); + web_listener_stop(sm); + while (sm->web_connections) + web_connection_stop(sm->web_connections); + while (sm->msearch_replies) + msearchreply_state_machine_stop(sm->msearch_replies); + while (sm->subscriptions) { + struct subscription *s = sm->subscriptions; + subscription_unlink(s); + subscription_destroy(s); + } + + advertisement_state_machine_stop(sm); + /* TODO: send byebye notifications */ + + event_send_stop_all(sm); + os_free(sm->wlanevent); + sm->wlanevent = NULL; + os_free(sm->net_if); + sm->net_if = NULL; + os_free(sm->mac_addr_text); + sm->mac_addr_text = NULL; + os_free(sm->ip_addr_text); + sm->ip_addr_text = NULL; + if (sm->multicast_sd >= 0) + close(sm->multicast_sd); + sm->multicast_sd = -1; + ssdp_listener_stop(sm); + + sm->started = 0; +} + + +/** + * upnp_wps_device_start - Start WPS UPnP operations on an interface + * @sm: WPS UPnP state machine from upnp_wps_device_init() + * @net_if: Selected network interface name + * Returns: 0 on success, -1 on failure + */ +int upnp_wps_device_start(struct upnp_wps_device_sm *sm, char *net_if) +{ + if (!sm || !net_if) + return -1; + + if (sm->started) + upnp_wps_device_stop(sm); + + sm->net_if = strdup(net_if); + sm->multicast_sd = -1; + sm->ssdp_sd = -1; + sm->started = 1; + sm->advertise_count = 0; + + /* Fix up linux multicast handling */ + if (add_ssdp_network(net_if)) + goto fail; + + /* Determine which IP and mac address we're using */ + if (get_netif_info(net_if, + &sm->ip_addr, &sm->ip_addr_text, + sm->mac_addr, &sm->mac_addr_text)) { + wpa_printf(MSG_INFO, "WPS UPnP: Could not get IP/MAC address " + "for %s. Does it have IP address?", net_if); + goto fail; + } + + /* Listen for incoming TCP connections so that others + * can fetch our "xml files" from us. + */ + if (web_listener_start(sm)) + goto fail; + + /* Set up for receiving discovery (UDP) packets */ + if (ssdp_listener_start(sm)) + goto fail; + + /* Set up for sending multicast */ + if (ssdp_open_multicast(sm) < 0) + goto fail; + + /* + * Broadcast NOTIFY messages to let the world know we exist. + * This is done via a state machine since the messages should not be + * all sent out at once. + */ + if (advertisement_state_machine_start(sm)) + goto fail; + + return 0; + +fail: + upnp_wps_device_stop(sm); + return -1; +} + + +/** + * upnp_wps_device_deinit - Deinitialize WPS UPnP + * @sm: WPS UPnP state machine from upnp_wps_device_init() + */ +void upnp_wps_device_deinit(struct upnp_wps_device_sm *sm) +{ + if (!sm) + return; + + upnp_wps_device_stop(sm); + + if (sm->peer.wps) + wps_deinit(sm->peer.wps); + os_free(sm->root_dir); + os_free(sm->desc_url); + os_free(sm->ctx); + os_free(sm); +} + + +/** + * upnp_wps_device_init - Initialize WPS UPnP + * @ctx: callback table; we must eventually free it + * @wps: Pointer to longterm WPS context + * @priv: External context data that will be used in callbacks + * Returns: WPS UPnP state or %NULL on failure + */ +struct upnp_wps_device_sm * +upnp_wps_device_init(struct upnp_wps_device_ctx *ctx, struct wps_context *wps, + void *priv) +{ + struct upnp_wps_device_sm *sm; + + sm = os_zalloc(sizeof(*sm)); + if (!sm) { + wpa_printf(MSG_ERROR, "WPS UPnP: upnp_wps_device_init failed"); + return NULL; + } + + sm->ctx = ctx; + sm->wps = wps; + sm->priv = priv; + + return sm; +} + + +/** + * upnp_wps_subscribers - Check whether there are any event subscribers + * @sm: WPS UPnP state machine from upnp_wps_device_init() + * Returns: 0 if no subscribers, 1 if subscribers + */ +int upnp_wps_subscribers(struct upnp_wps_device_sm *sm) +{ + return sm->subscriptions != NULL; +} diff --git a/src/wps/wps_upnp.h b/src/wps/wps_upnp.h new file mode 100644 index 000000000..995a7f2ac --- /dev/null +++ b/src/wps/wps_upnp.h @@ -0,0 +1,66 @@ +/* + * UPnP WPS Device + * Copyright (c) 2000-2003 Intel Corporation + * Copyright (c) 2006-2007 Sony Corporation + * Copyright (c) 2008-2009 Atheros Communications + * Copyright (c) 2009, Jouni Malinen + * + * See wps_upnp.c for more details on licensing and code history. + */ + +#ifndef WPS_UPNP_H +#define WPS_UPNP_H + +struct upnp_wps_device_sm; +struct wps_context; +struct wps_data; + +struct upnp_wps_peer { + struct wps_data *wps; +}; + +enum upnp_wps_wlanevent_type { + UPNP_WPS_WLANEVENT_TYPE_PROBE = 1, + UPNP_WPS_WLANEVENT_TYPE_EAP = 2 +}; + +struct upnp_wps_device_ctx { + struct wpabuf * (*rx_req_get_device_info)( + void *priv, struct upnp_wps_peer *peer); + struct wpabuf * (*rx_req_put_message)( + void *priv, struct upnp_wps_peer *peer, + const struct wpabuf *msg); + struct wpabuf * (*rx_req_get_ap_settings)(void *priv, + const struct wpabuf *msg); + int (*rx_req_set_ap_settings)(void *priv, const struct wpabuf *msg); + int (*rx_req_del_ap_settings)(void *priv, const struct wpabuf *msg); + struct wpabuf * (*rx_req_get_sta_settings)(void *priv, + const struct wpabuf *msg); + int (*rx_req_set_sta_settings)(void *priv, const struct wpabuf *msg); + int (*rx_req_del_sta_settings)(void *priv, const struct wpabuf *msg); + int (*rx_req_put_wlan_event_response)( + void *priv, enum upnp_wps_wlanevent_type ev_type, + const u8 *mac_addr, const struct wpabuf *msg); + int (*rx_req_set_selected_registrar)(void *priv, + const struct wpabuf *msg); + int (*rx_req_reboot_ap)(void *priv, const struct wpabuf *msg); + int (*rx_req_reset_ap)(void *priv, const struct wpabuf *msg); + int (*rx_req_reboot_sta)(void *priv, const struct wpabuf *msg); + int (*rx_req_reset_sta)(void *priv, const struct wpabuf *msg); +}; + +struct upnp_wps_device_sm * +upnp_wps_device_init(struct upnp_wps_device_ctx *ctx, struct wps_context *wps, + void *priv); +void upnp_wps_device_deinit(struct upnp_wps_device_sm *sm); + +int upnp_wps_device_start(struct upnp_wps_device_sm *sm, char *net_if); +void upnp_wps_device_stop(struct upnp_wps_device_sm *sm); + +int upnp_wps_device_send_wlan_event(struct upnp_wps_device_sm *sm, + const u8 from_mac_addr[ETH_ALEN], + enum upnp_wps_wlanevent_type ev_type, + const struct wpabuf *msg); +int upnp_wps_subscribers(struct upnp_wps_device_sm *sm); + +#endif /* WPS_UPNP_H */ diff --git a/src/wps/wps_upnp_event.c b/src/wps/wps_upnp_event.c new file mode 100644 index 000000000..7e7ce88e9 --- /dev/null +++ b/src/wps/wps_upnp_event.c @@ -0,0 +1,525 @@ +/* + * UPnP WPS Device - Event processing + * Copyright (c) 2000-2003 Intel Corporation + * Copyright (c) 2006-2007 Sony Corporation + * Copyright (c) 2008-2009 Atheros Communications + * Copyright (c) 2009, Jouni Malinen + * + * See wps_upnp.c for more details on licensing and code history. + */ + +#include "includes.h" +#include +#include + +#include "common.h" +#include "eloop.h" +#include "uuid.h" +#include "httpread.h" +#include "wps_upnp.h" +#include "wps_upnp_i.h" + +/* + * Event message generation (to subscribers) + * + * We make a separate copy for each message for each subscriber. This memory + * wasted could be limited (adding code complexity) by sharing copies, keeping + * a usage count and freeing when zero. + * + * Sending a message requires using a HTTP over TCP NOTIFY + * (like a PUT) which requires a number of states.. + */ + +#define MAX_EVENTS_QUEUED 20 /* How far behind queued events */ +#define EVENT_TIMEOUT_SEC 30 /* Drop sending event after timeout */ + +/* How long to wait before sending event */ +#define EVENT_DELAY_SECONDS 0 +#define EVENT_DELAY_MSEC 0 + +/* + * Event information that we send to each subscriber is remembered in this + * struct. The event cannot be sent by simple UDP; it has to be sent by a HTTP + * over TCP transaction which requires various states.. It may also need to be + * retried at a different address (if more than one is available). + * + * TODO: As an optimization we could share data between subscribers. + */ +struct wps_event_ { + struct wps_event_ *next; + struct wps_event_ *prev; /* double linked list */ + struct subscription *s; /* parent */ + unsigned subscriber_sequence; /* which event for this subscription*/ + int retry; /* which retry */ + struct subscr_addr *addr; /* address to connect to */ + struct wpabuf *data; /* event data to send */ + /* The following apply while we are sending an event message. */ + int sd; /* -1 or socket descriptor for open connection */ + int sd_registered; /* nonzero if we must cancel registration */ + struct httpread *hread; /* NULL or open connection for event msg */ +}; + + +static void event_timeout_handler(void *eloop_data, void *user_ctx); + +/* event_clean -- clean sockets etc. of event + * Leaves data, retry count etc. alone. + */ +static void event_clean(struct wps_event_ *e) +{ + if (e->s->current_event == e) { + eloop_cancel_timeout(event_timeout_handler, NULL, e); + e->s->current_event = NULL; + } + if (e->sd_registered) { + eloop_unregister_sock(e->sd, EVENT_TYPE_WRITE); + e->sd_registered = 0; + } + if (e->sd != -1) { + close(e->sd); + e->sd = -1; + } + if (e->hread) + httpread_destroy(e->hread); + e->hread = NULL; +} + + +/* event_delete -- delete single unqueued event + * (be sure to dequeue first if need be) + */ +void event_delete(struct wps_event_ *e) +{ + event_clean(e); + wpabuf_free(e->data); + os_free(e); +} + + +/* event_dequeue -- get next event from the queue + * Returns NULL if empty. + */ +static struct wps_event_ *event_dequeue(struct subscription *s) +{ + struct wps_event_ **event_head = &s->event_queue; + struct wps_event_ *e = *event_head; + if (e == NULL) + return NULL; + e->next->prev = e->prev; + e->prev->next = e->next; + if (*event_head == e) { + if (e == e->next) { + /* last in queue */ + *event_head = NULL; + } else { + *event_head = e->next; + } + } + s->n_queue--; + e->next = e->prev = NULL; + /* but parent "s" is still valid */ + return e; +} + + +/* event_enqueue_at_end -- add event to end of queue */ +static void event_enqueue_at_end(struct subscription *s, struct wps_event_ *e) +{ + struct wps_event_ **event_head = &s->event_queue; + if (*event_head == NULL) { + *event_head = e->next = e->prev = e; + } else { + e->next = *event_head; + e->prev = e->next->prev; + e->prev->next = e; + e->next->prev = e; + } + s->n_queue++; +} + + +/* event_enqueue_at_begin -- add event to begin of queue + * (appropriate for retrying event only) + */ +static void event_enqueue_at_begin(struct subscription *s, + struct wps_event_ *e) +{ + struct wps_event_ **event_head = &s->event_queue; + if (*event_head == NULL) { + *event_head = e->next = e->prev = e; + } else { + e->prev = *event_head; + e->next = e->prev->next; + e->prev->next = e; + e->next->prev = e; + *event_head = e; + } + s->n_queue++; +} + + +/* event_delete_all -- delete entire event queue and current event */ +void event_delete_all(struct subscription *s) +{ + struct wps_event_ *e; + while ((e = event_dequeue(s)) != NULL) + event_delete(e); + if (s->current_event) { + event_delete(s->current_event); + /* will set: s->current_event = NULL; */ + } +} + + +/** + * event_retry - Called when we had a failure delivering event msg + * @e: Event + * @do_next_address: skip address e.g. on connect fail + */ +static void event_retry(struct wps_event_ *e, int do_next_address) +{ + struct subscription *s = e->s; + struct upnp_wps_device_sm *sm = s->sm; + + event_clean(e); + /* will set: s->current_event = NULL; */ + + if (do_next_address) + e->retry++; + if (e->retry >= s->n_addr) { + wpa_printf(MSG_DEBUG, "WPS UPnP: Giving up on sending event"); + return; + } + event_enqueue_at_begin(s, e); + event_send_all_later(sm); +} + + +/* called if the overall event-sending process takes too long */ +static void event_timeout_handler(void *eloop_data, void *user_ctx) +{ + struct wps_event_ *e = user_ctx; + struct subscription *s = e->s; + + assert(e == s->current_event); + + wpa_printf(MSG_DEBUG, "WPS UPnP: Event send timeout"); + event_retry(e, 1); +} + + +/* event_got_response_handler -- called back when http response is received. */ +static void event_got_response_handler(struct httpread *handle, void *cookie, + enum httpread_event en) +{ + struct wps_event_ *e = cookie; + struct subscription *s = e->s; + struct upnp_wps_device_sm *sm = s->sm; + struct httpread *hread = e->hread; + int reply_code = 0; + + assert(e == s->current_event); + eloop_cancel_timeout(event_timeout_handler, NULL, e); + + if (en == HTTPREAD_EVENT_FILE_READY) { + if (httpread_hdr_type_get(hread) == HTTPREAD_HDR_TYPE_REPLY) { + reply_code = httpread_reply_code_get(hread); + if (reply_code == HTTP_OK) { + wpa_printf(MSG_DEBUG, + "WPS UPnP: Got event reply OK"); + event_delete(e); + goto send_more; + } else { + wpa_printf(MSG_DEBUG, "WPS UPnP: Got event " + "error reply code %d", reply_code); + goto bad; + } + } else { + wpa_printf(MSG_DEBUG, "WPS UPnP: Got bogus event " + "response %d", en); + } + } else { + wpa_printf(MSG_DEBUG, "WPS UPnP: Event response timeout/fail"); + goto bad; + } + event_retry(e, 1); + goto send_more; + +send_more: + /* Schedule sending more if there is more to send */ + if (s->event_queue) + event_send_all_later(sm); + return; + +bad: + /* + * If other side doesn't like what we say, forget about them. + * (There is no way to tell other side that we are dropping + * them...). + * Alternately, we could just do event_delete(e) + */ + wpa_printf(MSG_DEBUG, "WPS UPnP: Deleting subscription due to errors"); + subscription_unlink(s); + subscription_destroy(s); +} + + +/* event_send_tx_ready -- actually write event message + * + * Prequisite: subscription socket descriptor has become ready to + * write (because connection to subscriber has been made). + * + * It is also possible that we are called because the connect has failed; + * it is possible to test for this, or we can just go ahead and then + * the write will fail. + */ +static void event_send_tx_ready(int sock, void *eloop_ctx, void *sock_ctx) +{ + struct wps_event_ *e = sock_ctx; + struct subscription *s = e->s; + struct wpabuf *buf; + char *b; + + assert(e == s->current_event); + assert(e->sd == sock); + + buf = wpabuf_alloc(1000 + wpabuf_len(e->data)); + if (buf == NULL) { + event_retry(e, 0); + goto bad; + } + wpabuf_printf(buf, "NOTIFY %s HTTP/1.1\r\n", e->addr->path); + wpabuf_put_str(buf, "SERVER: Unspecified, UPnP/1.0, Unspecified\r\n"); + wpabuf_printf(buf, "HOST: %s\r\n", e->addr->domain_and_port); + wpabuf_put_str(buf, "CONTENT-TYPE: text/xml; charset=\"utf-8\"\r\n" + "NT: upnp:event\r\n" + "NTS: upnp:propchange\r\n"); + wpabuf_put_str(buf, "SID: uuid:"); + b = wpabuf_put(buf, 0); + uuid_bin2str(s->uuid, b, 80); + wpabuf_put(buf, os_strlen(b)); + wpabuf_put_str(buf, "\r\n"); + wpabuf_printf(buf, "SEQ: %u\r\n", e->subscriber_sequence); + wpabuf_printf(buf, "CONTENT-LENGTH: %d\r\n", + (int) wpabuf_len(e->data)); + wpabuf_put_str(buf, "\r\n"); /* terminating empty line */ + wpabuf_put_buf(buf, e->data); + + /* Since the message size is pretty small, we should be + * able to get the operating system to buffer what we give it + * and not have to come back again later to write more... + */ +#if 0 + /* we could: Turn blocking back on? */ + fcntl(e->sd, F_SETFL, 0); +#endif + if (send_wpabuf(e->sd, buf) < 0) { + event_retry(e, 1); + goto bad; + } + wpabuf_free(buf); + buf = NULL; + + if (e->sd_registered) { + e->sd_registered = 0; + eloop_unregister_sock(e->sd, EVENT_TYPE_WRITE); + } + /* Set up to read the reply */ + e->hread = httpread_create(e->sd, event_got_response_handler, + e /* cookie */, + 0 /* no data expected */, + EVENT_TIMEOUT_SEC); + if (e->hread == NULL) { + wpa_printf(MSG_ERROR, "WPS UPnP: httpread_create failed"); + event_retry(e, 0); + goto bad; + } + return; + +bad: + /* Schedule sending more if there is more to send */ + if (s->event_queue) + event_send_all_later(s->sm); + wpabuf_free(buf); +} + + +/* event_send_start -- prepare to send a event message to subscriber + * + * This gets complicated because: + * -- The message is sent via TCP and we have to keep the stream open + * for 30 seconds to get a response... then close it. + * -- But we might have other event happen in the meantime... + * we have to queue them, if we lose them then the subscriber will + * be forced to unsubscribe and subscribe again. + * -- If multiple URLs are provided then we are supposed to try successive + * ones after 30 second timeout. + * -- The URLs might use domain names instead of dotted decimal addresses, + * and resolution of those may cause unwanted sleeping. + * -- Doing the initial TCP connect can take a while, so we have to come + * back after connection and then send the data. + * + * Returns nonzero on error; + * + * Prerequisite: No current event send (s->current_event == NULL) + * and non-empty queue. + */ +static int event_send_start(struct subscription *s) +{ + struct wps_event_ *e; + int itry; + + /* + * Assume we are called ONLY with no current event and ONLY with + * nonempty event queue and ONLY with at least one address to send to. + */ + assert(s->addr_list != NULL); + assert(s->current_event == NULL); + assert(s->event_queue != NULL); + + s->current_event = e = event_dequeue(s); + + /* Use address acc. to no. of retries */ + e->addr = s->addr_list; + for (itry = 0; itry < e->retry; itry++) + e->addr = e->addr->next; + + e->sd = socket(AF_INET, SOCK_STREAM, 0); + if (e->sd < 0) { + event_retry(e, 0); + return -1; + } + /* set non-blocking so we don't sleep waiting for connection */ + if (fcntl(e->sd, F_SETFL, O_NONBLOCK) != 0) { + event_retry(e, 0); + return -1; + } + /* + * Start the connect. It might succeed immediately but more likely will + * return errno EINPROGRESS. + */ + if (connect(e->sd, (struct sockaddr *) &e->addr->saddr, + sizeof(e->addr->saddr))) { + if (errno == EINPROGRESS) { + } else { + event_retry(e, 1); + return -1; + } + } + /* Call back when ready for writing (or on failure...). */ + if (eloop_register_sock(e->sd, EVENT_TYPE_WRITE, event_send_tx_ready, + NULL, e)) { + event_retry(e, 0); + return -1; + } + e->sd_registered = 1; + /* Don't wait forever! */ + if (eloop_register_timeout(EVENT_TIMEOUT_SEC, 0, event_timeout_handler, + NULL, e)) { + event_retry(e, 0); + return -1; + } + return 0; +} + + +/* event_send_all_later_handler -- actually send events as needed */ +void event_send_all_later_handler(void *eloop_data, void *user_ctx) +{ + struct upnp_wps_device_sm *sm = user_ctx; + struct subscription *s; + struct subscription *s_old; + int nerrors = 0; + + sm->event_send_all_queued = 0; + if ((s = sm->subscriptions) == NULL) + return; + do { + if (s->addr_list == NULL) { + /* if we've given up on all addresses */ + wpa_printf(MSG_DEBUG, "WPS UPnP: Removing " + "subscription with no addresses"); + s_old = s; + s = s_old->next; + subscription_unlink(s_old); + subscription_destroy(s_old); + } else { + if (s->current_event == NULL /* not busy */ && + s->event_queue != NULL /* more to do */) { + if (event_send_start(s)) + nerrors++; + } + s = s->next; + } + } while (sm->subscriptions != NULL && s != sm->subscriptions); + + if (nerrors) { + /* Try again later */ + event_send_all_later(sm); + } +} + + +/* event_send_all_later -- schedule sending events to all subscribers + * that need it. + * This avoids two problems: + * -- After getting a subscription, we should not send the first event + * until after our reply is fully queued to be sent back, + * -- Possible stack depth or infinite recursion issues. + */ +void event_send_all_later(struct upnp_wps_device_sm *sm) +{ + /* + * The exact time in the future isn't too important. Waiting a bit + * might let us do several together. + */ + if (sm->event_send_all_queued) + return; + sm->event_send_all_queued = 1; + eloop_register_timeout(EVENT_DELAY_SECONDS, EVENT_DELAY_MSEC, + event_send_all_later_handler, NULL, sm); +} + + +/* event_send_stop_all -- cleanup */ +void event_send_stop_all(struct upnp_wps_device_sm *sm) +{ + if (sm->event_send_all_queued) + eloop_cancel_timeout(event_send_all_later_handler, NULL, sm); + sm->event_send_all_queued = 0; +} + + +/** + * event_add - Add a new event to a queue + * @s: Subscription + * @data: Event data (is copied; caller retains ownership) + * Returns: 0 on success, 1 on error + */ +int event_add(struct subscription *s, const struct wpabuf *data) +{ + struct wps_event_ *e; + + if (s->n_queue >= MAX_EVENTS_QUEUED) { + wpa_printf(MSG_DEBUG, "WPS UPnP: Too many events queued for " + "subscriber"); + return 1; + } + + e = os_zalloc(sizeof(*e)); + if (e == NULL) + return 1; + e->s = s; + e->sd = -1; + e->data = wpabuf_dup(data); + if (e->data == NULL) { + os_free(e); + return 1; + } + e->subscriber_sequence = s->next_subscriber_sequence++; + if (s->next_subscriber_sequence == 0) + s->next_subscriber_sequence++; + event_enqueue_at_end(s, e); + event_send_all_later(s->sm); + return 0; +} diff --git a/src/wps/wps_upnp_i.h b/src/wps/wps_upnp_i.h new file mode 100644 index 000000000..d4b6569e1 --- /dev/null +++ b/src/wps/wps_upnp_i.h @@ -0,0 +1,193 @@ +/* + * UPnP for WPS / internal definitions + * Copyright (c) 2000-2003 Intel Corporation + * Copyright (c) 2006-2007 Sony Corporation + * Copyright (c) 2008-2009 Atheros Communications + * Copyright (c) 2009, Jouni Malinen + * + * See wps_upnp.c for more details on licensing and code history. + */ + +#ifndef WPS_UPNP_I_H +#define WPS_UPNP_I_H + +#define UPNP_MULTICAST_ADDRESS "239.255.255.250" /* for UPnP multicasting */ +#define UPNP_MULTICAST_PORT 1900 /* UDP port to monitor for UPnP */ + +/* min subscribe time per UPnP standard */ +#define UPNP_SUBSCRIBE_SEC_MIN 1800 +/* subscribe time we use */ +#define UPNP_SUBSCRIBE_SEC (UPNP_SUBSCRIBE_SEC_MIN + 1) + +/* "filenames" used in URLs that we service via our "web server": */ +#define UPNP_WPS_DEVICE_XML_FILE "wps_device.xml" +#define UPNP_WPS_SCPD_XML_FILE "wps_scpd.xml" +#define UPNP_WPS_DEVICE_CONTROL_FILE "wps_control" +#define UPNP_WPS_DEVICE_EVENT_FILE "wps_event" + + +struct web_connection; +struct subscription; +struct upnp_wps_device_sm; + + +enum http_reply_code { + HTTP_OK = 200, + HTTP_BAD_REQUEST = 400, + UPNP_INVALID_ACTION = 401, + UPNP_INVALID_ARGS = 402, + HTTP_PRECONDITION_FAILED = 412, + HTTP_INTERNAL_SERVER_ERROR = 500, + HTTP_UNIMPLEMENTED = 501, + UPNP_ACTION_FAILED = 501, + UPNP_ARG_VALUE_INVALID = 600, + UPNP_ARG_VALUE_OUT_OF_RANGE = 601, + UPNP_OUT_OF_MEMORY = 603 +}; + + +enum advertisement_type_enum { + ADVERTISE_UP = 0, + ADVERTISE_DOWN = 1, + MSEARCH_REPLY = 2 +}; + +/* + * Advertisements are broadcast via UDP NOTIFYs, and are also the essence of + * the reply to UDP M-SEARCH requests. This struct handles both cases. + * + * A state machine is needed because a number of variant forms must be sent in + * separate packets and spread out in time to avoid congestion. + */ +struct advertisement_state_machine { + /* double-linked list */ + struct advertisement_state_machine *next; + struct advertisement_state_machine *prev; + struct upnp_wps_device_sm *sm; /* parent */ + enum advertisement_type_enum type; + int state; + int nerrors; + struct sockaddr_in client; /* for M-SEARCH replies */ +}; + + +/* + * An address of a subscriber (who may have multiple addresses). We are + * supposed to send (via TCP) updates to each subscriber, trying each address + * for a subscriber until we find one that seems to work. + */ +struct subscr_addr { + /* double linked list */ + struct subscr_addr *next; + struct subscr_addr *prev; + struct subscription *s; /* parent */ + char *domain_and_port; /* domain and port part of url */ + char *path; /* "filepath" part of url (from "mem") */ + struct sockaddr_in saddr; /* address for doing connect */ +}; + + +/* + * Subscribers to our events are recorded in this struct. This includes a max + * of one outgoing connection (sending an "event message") per subscriber. We + * also have to age out subscribers unless they renew. + */ +struct subscription { + /* double linked list */ + struct subscription *next; + struct subscription *prev; + struct upnp_wps_device_sm *sm; /* parent */ + time_t timeout_time; /* when to age out the subscription */ + unsigned next_subscriber_sequence; /* number our messages */ + /* + * This uuid identifies the subscription and is randomly generated by + * us and given to the subscriber when the subscription is accepted; + * and is then included with each event sent to the subscriber. + */ + u8 uuid[UUID_LEN]; + /* Linked list of address alternatives (rotate through on failure) */ + struct subscr_addr *addr_list; + int n_addr; /* Number of addresses in list */ + struct wps_event_ *event_queue; /* Queued event messages. */ + int n_queue; /* How many events are queued */ + struct wps_event_ *current_event; /* non-NULL if being sent (not in q) + */ +}; + + +/* + * Our instance data corresponding to one WiFi network interface + * (multiple might share the same wired network interface!). + * + * This is known as an opaque struct declaration to users of the WPS UPnP code. + */ +struct upnp_wps_device_sm { + struct upnp_wps_device_ctx *ctx; /* callback table */ + struct wps_context *wps; + void *priv; + char *root_dir; + char *desc_url; + int started; /* nonzero if we are active */ + char *net_if; /* network interface we use */ + char *mac_addr_text; /* mac addr of network i.f. we use */ + u8 mac_addr[ETH_ALEN]; /* mac addr of network i.f. we use */ + char *ip_addr_text; /* IP address of network i.f. we use */ + unsigned ip_addr; /* IP address of network i.f. we use (host order) */ + int multicast_sd; /* send multicast messages over this socket */ + int ssdp_sd; /* receive discovery UPD packets on socket */ + int ssdp_sd_registered; /* nonzero if we must unregister */ + unsigned advertise_count; /* how many advertisements done */ + struct advertisement_state_machine advertisement; + struct advertisement_state_machine *msearch_replies; + int n_msearch_replies; /* no. of pending M-SEARCH replies */ + int web_port; /* our port that others get xml files from */ + int web_sd; /* socket to listen for web requests */ + int web_sd_registered; /* nonzero if we must cancel registration */ + struct web_connection *web_connections; /* linked list */ + int n_web_connections; /* no. of pending web connections */ + /* Note: subscriptions are kept in expiry order */ + struct subscription *subscriptions; /* linked list */ + int n_subscriptions; /* no of current subscriptions */ + int event_send_all_queued; /* if we are scheduled to send events soon + */ + + char *wlanevent; /* the last WLANEvent data */ + + /* FIX: maintain separate structures for each UPnP peer */ + struct upnp_wps_peer peer; +}; + +/* wps_upnp.c */ +void format_date(struct wpabuf *buf); +struct subscription * subscription_start(struct upnp_wps_device_sm *sm, + char *callback_urls); +struct subscription * subscription_renew(struct upnp_wps_device_sm *sm, + const u8 uuid[UUID_LEN]); +void subscription_unlink(struct subscription *s); +void subscription_destroy(struct subscription *s); +struct subscription * subscription_find(struct upnp_wps_device_sm *sm, + const u8 uuid[UUID_LEN]); +int send_wpabuf(int fd, struct wpabuf *buf); + +/* wps_upnp_ssdp.c */ +void msearchreply_state_machine_stop(struct advertisement_state_machine *a); +int advertisement_state_machine_start(struct upnp_wps_device_sm *sm); +void advertisement_state_machine_stop(struct upnp_wps_device_sm *sm); +void ssdp_listener_stop(struct upnp_wps_device_sm *sm); +int ssdp_listener_start(struct upnp_wps_device_sm *sm); +int add_ssdp_network(char *net_if); +int ssdp_open_multicast(struct upnp_wps_device_sm *sm); + +/* wps_upnp_web.c */ +void web_connection_stop(struct web_connection *c); +int web_listener_start(struct upnp_wps_device_sm *sm); +void web_listener_stop(struct upnp_wps_device_sm *sm); + +/* wps_upnp_event.c */ +int event_add(struct subscription *s, const struct wpabuf *data); +void event_delete(struct wps_event_ *e); +void event_delete_all(struct subscription *s); +void event_send_all_later(struct upnp_wps_device_sm *sm); +void event_send_stop_all(struct upnp_wps_device_sm *sm); + +#endif /* WPS_UPNP_I_H */ diff --git a/src/wps/wps_upnp_ssdp.c b/src/wps/wps_upnp_ssdp.c new file mode 100644 index 000000000..71dcc9645 --- /dev/null +++ b/src/wps/wps_upnp_ssdp.c @@ -0,0 +1,887 @@ +/* + * UPnP SSDP for WPS + * Copyright (c) 2000-2003 Intel Corporation + * Copyright (c) 2006-2007 Sony Corporation + * Copyright (c) 2008-2009 Atheros Communications + * Copyright (c) 2009, Jouni Malinen + * + * See wps_upnp.c for more details on licensing and code history. + */ + +#include "includes.h" + +#include +#include +#include + +#include "common.h" +#include "uuid.h" +#include "eloop.h" +#include "wps.h" +#include "wps_upnp.h" +#include "wps_upnp_i.h" + +#define UPNP_CACHE_SEC (UPNP_CACHE_SEC_MIN + 1) /* cache time we use */ +#define UPNP_CACHE_SEC_MIN 1800 /* min cachable time per UPnP standard */ +#define UPNP_ADVERTISE_REPEAT 2 /* no more than 3 */ +#define MULTICAST_MAX_READ 1600 /* max bytes we'll read for UPD request */ +#define MAX_MSEARCH 20 /* max simultaneous M-SEARCH replies ongoing */ +#define SSDP_TARGET "239.0.0.0" +#define SSDP_NETMASK "255.0.0.0" + + +/* Check tokens for equality, where tokens consist of letters, digits, + * underscore and hyphen, and are matched case insensitive. + */ +static int token_eq(const char *s1, const char *s2) +{ + int c1; + int c2; + int end1 = 0; + int end2 = 0; + for (;;) { + c1 = *s1++; + c2 = *s2++; + if (isalpha(c1) && isupper(c1)) + c1 = tolower(c1); + if (isalpha(c2) && isupper(c2)) + c2 = tolower(c2); + end1 = !(isalnum(c1) || c1 == '_' || c1 == '-'); + end2 = !(isalnum(c2) || c2 == '_' || c2 == '-'); + if (end1 || end2 || c1 != c2) + break; + } + return (end1 && end2); /* reached end of both words? */ +} + + +/* Return length of token (see above for definition of token) */ +static int token_length(const char *s) +{ + const char *begin = s; + for (;; s++) { + int c = *s; + int end = !(isalnum(c) || c == '_' || c == '-'); + if (end) + break; + } + return s - begin; +} + + +/* return length of interword separation. + * This accepts only spaces/tabs and thus will not traverse a line + * or buffer ending. + */ +static int word_separation_length(const char *s) +{ + const char *begin = s; + for (;; s++) { + int c = *s; + if (c == ' ' || c == '\t') + continue; + break; + } + return s - begin; +} + + +/* No. of chars through (including) end of line */ +static int line_length(const char *l) +{ + const char *lp = l; + while (*lp && *lp != '\n') + lp++; + if (*lp == '\n') + lp++; + return lp - l; +} + + +/* No. of chars excluding trailing whitespace */ +static int line_length_stripped(const char *l) +{ + const char *lp = l + line_length(l); + while (lp > l && !isgraph(lp[-1])) + lp--; + return lp - l; +} + + +static int str_starts(const char *str, const char *start) +{ + return os_strncmp(str, start, os_strlen(start)) == 0; +} + + +/*************************************************************************** + * Advertisements. + * These are multicast to the world to tell them we are here. + * The individual packets are spread out in time to limit loss, + * and then after a much longer period of time the whole sequence + * is repeated again (for NOTIFYs only). + **************************************************************************/ + +/** + * next_advertisement - Build next message and advance the state machine + * @a: Advertisement state + * @islast: Buffer for indicating whether this is the last message (= 1) + * Returns: The new message (caller is responsible for freeing this) + * + * Note: next_advertisement is shared code with msearchreply_* functions + */ +static struct wpabuf * +next_advertisement(struct advertisement_state_machine *a, int *islast) +{ + struct wpabuf *msg; + char *NTString = ""; + char uuid_string[80]; + + *islast = 0; + uuid_bin2str(a->sm->wps->uuid, uuid_string, sizeof(uuid_string)); + msg = wpabuf_alloc(800); /* more than big enough */ + if (msg == NULL) + goto fail; + switch (a->type) { + case ADVERTISE_UP: + case ADVERTISE_DOWN: + NTString = "NT"; + wpabuf_put_str(msg, "NOTIFY * HTTP/1.1\r\n"); + wpabuf_printf(msg, "HOST: %s:%d\r\n", + UPNP_MULTICAST_ADDRESS, UPNP_MULTICAST_PORT); + wpabuf_printf(msg, "CACHE-CONTROL: max-age=%d\r\n", + UPNP_CACHE_SEC); + wpabuf_printf(msg, "NTS: %s\r\n", + (a->type == ADVERTISE_UP ? + "ssdp:alive" : "ssdp:byebye")); + break; + case MSEARCH_REPLY: + NTString = "ST"; + wpabuf_put_str(msg, "HTTP/1.1 200 OK\r\n"); + wpabuf_printf(msg, "CACHE-CONTROL: max-age=%d\r\n", + UPNP_CACHE_SEC); + + wpabuf_put_str(msg, "DATE: "); + format_date(msg); + wpabuf_put_str(msg, "\r\n"); + + wpabuf_put_str(msg, "EXT:\r\n"); + break; + } + + if (a->type != ADVERTISE_DOWN) { + /* Where others may get our XML files from */ + wpabuf_printf(msg, "LOCATION: http://%s:%d/%s\r\n", + a->sm->ip_addr_text, a->sm->web_port, + UPNP_WPS_DEVICE_XML_FILE); + } + + /* The SERVER line has three comma-separated fields: + * operating system / version + * upnp version + * software package / version + * However, only the UPnP version is really required, the + * others can be place holders... for security reasons + * it is better to NOT provide extra information. + */ + wpabuf_put_str(msg, "SERVER: Unspecified, UPnP/1.0, Unspecified\r\n"); + + switch (a->state / UPNP_ADVERTISE_REPEAT) { + case 0: + wpabuf_printf(msg, "%s: upnp:rootdevice\r\n", NTString); + wpabuf_printf(msg, "USN: uuid:%s::upnp:rootdevice\r\n", + uuid_string); + break; + case 1: + wpabuf_printf(msg, "%s: uuid:%s\r\n", NTString, uuid_string); + wpabuf_printf(msg, "USN: uuid:%s\r\n", uuid_string); + break; + case 2: + wpabuf_printf(msg, "%s: urn:schemas-wifialliance-org:device:" + "WFADevice:1\r\n", NTString); + wpabuf_printf(msg, "USN: uuid:%s::urn:schemas-wifialliance-" + "org:device:WFADevice:1\r\n", uuid_string); + break; + case 3: + wpabuf_printf(msg, "%s: urn:schemas-wifialliance-org:service:" + "WFAWLANConfig:1\r\n", NTString); + wpabuf_printf(msg, "USN: uuid:%s::urn:schemas-wifialliance-" + "org:service:WFAWLANConfig:1\r\n", uuid_string); + break; + } + wpabuf_put_str(msg, "\r\n"); + + if (a->state + 1 >= 4 * UPNP_ADVERTISE_REPEAT) + *islast = 1; + + return msg; + +fail: + wpabuf_free(msg); + return NULL; +} + + +static void advertisement_state_machine_handler(void *eloop_data, + void *user_ctx); + + +/** + * advertisement_state_machine_stop - Stop SSDP advertisements + * @sm: WPS UPnP state machine from upnp_wps_device_init() + */ +void advertisement_state_machine_stop(struct upnp_wps_device_sm *sm) +{ + eloop_cancel_timeout(advertisement_state_machine_handler, NULL, sm); +} + + +static void advertisement_state_machine_handler(void *eloop_data, + void *user_ctx) +{ + struct upnp_wps_device_sm *sm = user_ctx; + struct advertisement_state_machine *a = &sm->advertisement; + struct wpabuf *msg; + int next_timeout_msec = 100; + int next_timeout_sec = 0; + struct sockaddr_in dest; + int islast = 0; + + /* + * Each is sent twice (in case lost) w/ 100 msec delay between; + * spec says no more than 3 times. + * One pair for rootdevice, one pair for uuid, and a pair each for + * each of the two urns. + * The entire sequence must be repeated before cache control timeout + * (which is min 1800 seconds), + * recommend random portion of half of the advertised cache control age + * to ensure against loss... perhaps 1800/4 + rand*1800/4 ? + * Delay random interval < 100 msec prior to initial sending. + * TTL of 4 + */ + + wpa_printf(MSG_MSGDUMP, "WPS UPnP: Advertisement state=%d", a->state); + msg = next_advertisement(a, &islast); + if (msg == NULL) + return; + + os_memset(&dest, 0, sizeof(dest)); + dest.sin_family = AF_INET; + dest.sin_addr.s_addr = inet_addr(UPNP_MULTICAST_ADDRESS); + dest.sin_port = htons(UPNP_MULTICAST_PORT); + + if (sendto(sm->multicast_sd, wpabuf_head(msg), wpabuf_len(msg), 0, + (struct sockaddr *) &dest, sizeof(dest)) == -1) { + wpa_printf(MSG_ERROR, "WPS UPnP: Advertisement sendto failed:" + "%d (%s)", errno, strerror(errno)); + next_timeout_msec = 0; + next_timeout_sec = 10; /* ... later */ + } else if (islast) { + a->state = 0; /* wrap around */ + if (a->type == ADVERTISE_DOWN) { + wpa_printf(MSG_DEBUG, "WPS UPnP: ADVERTISE_DOWN->UP"); + a->type = ADVERTISE_UP; + /* do it all over again right away */ + } else { + u16 r; + /* + * Start over again after a long timeout + * (see notes above) + */ + next_timeout_msec = 0; + os_get_random((void *) &r, sizeof(r)); + next_timeout_sec = UPNP_CACHE_SEC / 4 + + (((UPNP_CACHE_SEC / 4) * r) >> 16); + sm->advertise_count++; + wpa_printf(MSG_DEBUG, "WPS UPnP: ADVERTISE_UP (#%u); " + "next in %d sec", + sm->advertise_count, next_timeout_sec); + } + } else { + a->state++; + } + + wpabuf_free(msg); + + eloop_register_timeout(next_timeout_sec, next_timeout_msec, + advertisement_state_machine_handler, NULL, sm); +} + + +/** + * advertisement_state_machine_start - Start SSDP advertisements + * @sm: WPS UPnP state machine from upnp_wps_device_init() + * Returns: 0 on success, -1 on failure + */ +int advertisement_state_machine_start(struct upnp_wps_device_sm *sm) +{ + struct advertisement_state_machine *a = &sm->advertisement; + int next_timeout_msec; + + advertisement_state_machine_stop(sm); + + /* + * Start out advertising down, this automatically switches + * to advertising up which signals our restart. + */ + a->type = ADVERTISE_DOWN; + a->state = 0; + a->sm = sm; + /* (other fields not used here) */ + + /* First timeout should be random interval < 100 msec */ + next_timeout_msec = (100 * (os_random() & 0xFF)) >> 8; + return eloop_register_timeout(0, next_timeout_msec, + advertisement_state_machine_handler, + NULL, sm); +} + + +/*************************************************************************** + * M-SEARCH replies + * These are very similar to the multicast advertisements, with some + * small changes in data content; and they are sent (UDP) to a specific + * unicast address instead of multicast. + * They are sent in response to a UDP M-SEARCH packet. + **************************************************************************/ + +static void msearchreply_state_machine_handler(void *eloop_data, + void *user_ctx); + + +/** + * msearchreply_state_machine_stop - Stop M-SEARCH reply state machine + * @a: Selected advertisement/reply state + */ +void msearchreply_state_machine_stop(struct advertisement_state_machine *a) +{ + struct upnp_wps_device_sm *sm = a->sm; + wpa_printf(MSG_DEBUG, "WPS UPnP: M-SEARCH stop"); + if (a->next == a) { + sm->msearch_replies = NULL; + } else { + if (sm->msearch_replies == a) { + sm->msearch_replies = a->next; + } + a->next->prev = a->prev; + a->prev->next = a->next; + } + os_free(a); + sm->n_msearch_replies--; +} + + +static void msearchreply_state_machine_handler(void *eloop_data, + void *user_ctx) +{ + struct advertisement_state_machine *a = user_ctx; + struct upnp_wps_device_sm *sm = a->sm; + struct wpabuf *msg; + int next_timeout_msec = 100; + int next_timeout_sec = 0; + int islast = 0; + + /* + * Each response is sent twice (in case lost) w/ 100 msec delay + * between; spec says no more than 3 times. + * One pair for rootdevice, one pair for uuid, and a pair each for + * each of the two urns. + */ + + /* TODO: should only send the requested response types */ + + wpa_printf(MSG_MSGDUMP, "WPS UPnP: M-SEARCH reply state=%d (%s:%d)", + a->state, inet_ntoa(a->client.sin_addr), + ntohs(a->client.sin_port)); + msg = next_advertisement(a, &islast); + if (msg == NULL) + return; + + /* + * Send it on the multicast socket to avoid having to set up another + * socket. + */ + if (sendto(sm->multicast_sd, wpabuf_head(msg), wpabuf_len(msg), 0, + (struct sockaddr *) &a->client, sizeof(a->client)) < 0) { + wpa_printf(MSG_DEBUG, "WPS UPnP: M-SEARCH reply sendto " + "errno %d (%s) for %s:%d", + errno, strerror(errno), + inet_ntoa(a->client.sin_addr), + ntohs(a->client.sin_port)); + /* Ignore error and hope for the best */ + } + wpabuf_free(msg); + if (islast) { + wpa_printf(MSG_DEBUG, "WPS UPnP: M-SEARCH reply done"); + msearchreply_state_machine_stop(a); + return; + } + a->state++; + + wpa_printf(MSG_MSGDUMP, "WPS UPnP: M-SEARCH reply in %d.%03d sec", + next_timeout_sec, next_timeout_msec); + eloop_register_timeout(next_timeout_sec, next_timeout_msec, + msearchreply_state_machine_handler, sm, a); +} + + +/** + * msearchreply_state_machine_start - Reply to M-SEARCH discovery request + * @sm: WPS UPnP state machine from upnp_wps_device_init() + * @client: Client address + * @mx: Maximum delay in seconds + * + * Use TTL of 4 (this was done when socket set up). + * A response should be given in randomized portion of min(MX,120) seconds + * + * UPnP-arch-DeviceArchitecture, 1.2.3: + * To be found, a device must send a UDP response to the source IP address and + * port that sent the request to the multicast channel. Devices respond if the + * ST header of the M-SEARCH request is "ssdp:all", "upnp:rootdevice", "uuid:" + * followed by a UUID that exactly matches one advertised by the device. + */ +static void msearchreply_state_machine_start(struct upnp_wps_device_sm *sm, + struct sockaddr_in *client, + int mx) +{ + struct advertisement_state_machine *a; + int next_timeout_sec; + int next_timeout_msec; + + wpa_printf(MSG_DEBUG, "WPS UPnP: M-SEARCH reply start (%d " + "outstanding)", sm->n_msearch_replies); + if (sm->n_msearch_replies >= MAX_MSEARCH) { + wpa_printf(MSG_INFO, "WPS UPnP: Too many outstanding " + "M-SEARCH replies"); + return; + } + + a = os_zalloc(sizeof(*a)); + if (a == NULL) + return; + a->type = MSEARCH_REPLY; + a->state = 0; + a->sm = sm; + os_memcpy(&a->client, client, sizeof(client)); + /* Wait time depending on MX value */ + next_timeout_msec = (1000 * mx * (os_random() & 0xFF)) >> 8; + next_timeout_sec = next_timeout_msec / 1000; + next_timeout_msec = next_timeout_msec % 1000; + if (eloop_register_timeout(next_timeout_sec, next_timeout_msec, + msearchreply_state_machine_handler, sm, + a)) { + /* No way to recover (from malloc failure) */ + goto fail; + } + /* Remember for future cleanup */ + if (sm->msearch_replies) { + a->next = sm->msearch_replies; + a->prev = a->next->prev; + a->prev->next = a; + a->next->prev = a; + } else { + sm->msearch_replies = a->next = a->prev = a; + } + sm->n_msearch_replies++; + return; + +fail: + wpa_printf(MSG_INFO, "WPS UPnP: M-SEARCH reply failure!"); + eloop_cancel_timeout(msearchreply_state_machine_handler, sm, a); + os_free(a); +} + + +/** + * ssdp_parse_msearch - Process a received M-SEARCH + * @sm: WPS UPnP state machine from upnp_wps_device_init() + * @client: Client address + * @data: NULL terminated M-SEARCH message + * + * Given that we have received a header w/ M-SEARCH, act upon it + * + * Format of M-SEARCH (case insensitive!): + * + * First line must be: + * M-SEARCH * HTTP/1.1 + * Other lines in arbitrary order: + * HOST:239.255.255.250:1900 + * ST: + * MAN:"ssdp:discover" + * MX: + * + * It should be noted that when Microsoft Vista is still learning its IP + * address, it sends out host lines like: HOST:[FF02::C]:1900 + */ +static void ssdp_parse_msearch(struct upnp_wps_device_sm *sm, + struct sockaddr_in *client, const char *data) +{ + const char *start = data; + const char *end; + int got_host = 0; + int got_st = 0, st_match = 0; + int got_man = 0; + int got_mx = 0; + int mx = 0; + + /* + * Skip first line M-SEARCH * HTTP/1.1 + * (perhaps we should check remainder of the line for syntax) + */ + data += line_length(data); + + /* Parse remaining lines */ + for (; *data != '\0'; data += line_length(data)) { + end = data + line_length_stripped(data); + if (token_eq(data, "host")) { + /* The host line indicates who the packet + * is addressed to... but do we really care? + * Note that Microsoft sometimes does funny + * stuff with the HOST: line. + */ +#if 0 /* could be */ + data += token_length(data); + data += word_separation_length(data); + if (*data != ':') + goto bad; + data++; + data += word_separation_length(data); + /* UPNP_MULTICAST_ADDRESS */ + if (!str_starts(data, "239.255.255.250")) + goto bad; + data += os_strlen("239.255.255.250"); + if (*data == ':') { + if (!str_starts(data, ":1900")) + goto bad; + } +#endif /* could be */ + got_host = 1; + continue; + } else if (token_eq(data, "st")) { + /* There are a number of forms; we look + * for one that matches our case. + */ + got_st = 1; + data += token_length(data); + data += word_separation_length(data); + if (*data != ':') + continue; + data++; + data += word_separation_length(data); + if (str_starts(data, "ssdp:all")) { + st_match = 1; + continue; + } + if (str_starts(data, "upnp:rootdevice")) { + st_match = 1; + continue; + } + if (str_starts(data, "uuid:")) { + char uuid_string[80]; + data += os_strlen("uuid:"); + uuid_bin2str(sm->wps->uuid, uuid_string, + sizeof(uuid_string)); + if (str_starts(data, uuid_string)) + st_match = 1; + continue; + } +#if 0 + /* FIX: should we really reply to IGD string? */ + if (str_starts(data, "urn:schemas-upnp-org:device:" + "InternetGatewayDevice:1")) { + st_match = 1; + continue; + } +#endif + if (str_starts(data, "urn:schemas-wifialliance-org:" + "service:WFAWLANConfig:1")) { + st_match = 1; + continue; + } + if (str_starts(data, "urn:schemas-wifialliance-org:" + "device:WFADevice:1")) { + st_match = 1; + continue; + } + continue; + } else if (token_eq(data, "man")) { + data += token_length(data); + data += word_separation_length(data); + if (*data != ':') + continue; + data++; + data += word_separation_length(data); + if (!str_starts(data, "\"ssdp:discover\"")) { + wpa_printf(MSG_DEBUG, "WPS UPnP: Unexpected " + "M-SEARCH man-field"); + goto bad; + } + got_man = 1; + continue; + } else if (token_eq(data, "mx")) { + data += token_length(data); + data += word_separation_length(data); + if (*data != ':') + continue; + data++; + data += word_separation_length(data); + mx = atol(data); + got_mx = 1; + continue; + } + /* ignore anything else */ + } + if (!got_host || !got_st || !got_man || !got_mx || mx < 0) { + wpa_printf(MSG_DEBUG, "WPS UPnP: Invalid M-SEARCH: %d %d %d " + "%d mx=%d", got_host, got_st, got_man, got_mx, mx); + goto bad; + } + if (!st_match) { + wpa_printf(MSG_DEBUG, "WPS UPnP: Ignored M-SEARCH (no ST " + "match)"); + return; + } + if (mx > 120) + mx = 120; /* UPnP-arch-DeviceArchitecture, 1.2.3 */ + msearchreply_state_machine_start(sm, client, mx); + return; + +bad: + wpa_printf(MSG_INFO, "WPS UPnP: Failed to parse M-SEARCH"); + wpa_printf(MSG_MSGDUMP, "WPS UPnP: M-SEARCH data:\n%s", start); +} + + +/* Listening for (UDP) discovery (M-SEARCH) packets */ + +/** + * ssdp_listener_stop - Stop SSDP listered + * @sm: WPS UPnP state machine from upnp_wps_device_init() + * + * This function stops the SSDP listerner that was started by calling + * ssdp_listener_start(). + */ +void ssdp_listener_stop(struct upnp_wps_device_sm *sm) +{ + if (sm->ssdp_sd_registered) { + eloop_unregister_sock(sm->ssdp_sd, EVENT_TYPE_READ); + sm->ssdp_sd_registered = 0; + } + + if (sm->ssdp_sd != -1) { + close(sm->ssdp_sd); + sm->ssdp_sd = -1; + } + + eloop_cancel_timeout(msearchreply_state_machine_handler, sm, + ELOOP_ALL_CTX); +} + + +static void ssdp_listener_handler(int sd, void *eloop_ctx, void *sock_ctx) +{ + struct upnp_wps_device_sm *sm = sock_ctx; + struct sockaddr_in addr; /* client address */ + socklen_t addr_len; + int nread; + char buf[MULTICAST_MAX_READ], *pos; + + addr_len = sizeof(addr); + nread = recvfrom(sm->ssdp_sd, buf, sizeof(buf) - 1, 0, + (struct sockaddr *) &addr, &addr_len); + if (nread <= 0) + return; + buf[nread] = '\0'; /* need null termination for algorithm */ + + if (str_starts(buf, "NOTIFY ")) { + /* + * Silently ignore NOTIFYs to avoid filling debug log with + * unwanted messages. + */ + return; + } + + pos = os_strchr(buf, '\n'); + if (pos) + *pos = '\0'; + wpa_printf(MSG_MSGDUMP, "WPS UPnP: Received SSDP packet from %s:%d: " + "%s", inet_ntoa(addr.sin_addr), ntohs(addr.sin_port), buf); + if (pos) + *pos = '\n'; + + /* Parse first line */ + if (os_strncasecmp(buf, "M-SEARCH", os_strlen("M-SEARCH")) == 0 && + !isgraph(buf[strlen("M-SEARCH")])) { + ssdp_parse_msearch(sm, &addr, buf); + return; + } + + /* Ignore anything else */ +} + + +/** + * ssdp_listener_start - Set up for receiving discovery (UDP) packets + * @sm: WPS UPnP state machine from upnp_wps_device_init() + * Returns: 0 on success, -1 on failure + * + * The SSDP listerner is stopped by calling ssdp_listener_stop(). + */ +int ssdp_listener_start(struct upnp_wps_device_sm *sm) +{ + int sd = -1; + struct sockaddr_in addr; + struct ip_mreq mcast_addr; + int on = 1; + /* per UPnP spec, keep IP packet time to live (TTL) small */ + unsigned char ttl = 4; + + sm->ssdp_sd = sd = socket(AF_INET, SOCK_DGRAM, 0); + if (sd < 0) + goto fail; + if (fcntl(sd, F_SETFL, O_NONBLOCK) != 0) + goto fail; + if (setsockopt(sd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on))) + goto fail; + os_memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_ANY); + addr.sin_port = htons(UPNP_MULTICAST_PORT); + if (bind(sd, (struct sockaddr *) &addr, sizeof(addr))) + goto fail; + os_memset(&mcast_addr, 0, sizeof(mcast_addr)); + mcast_addr.imr_interface.s_addr = htonl(INADDR_ANY); + mcast_addr.imr_multiaddr.s_addr = inet_addr(UPNP_MULTICAST_ADDRESS); + if (setsockopt(sd, IPPROTO_IP, IP_ADD_MEMBERSHIP, + (char *) &mcast_addr, sizeof(mcast_addr))) + goto fail; + if (setsockopt(sd, IPPROTO_IP, IP_MULTICAST_TTL, + &ttl, sizeof(ttl))) + goto fail; + if (eloop_register_sock(sd, EVENT_TYPE_READ, ssdp_listener_handler, + NULL, sm)) + goto fail; + sm->ssdp_sd_registered = 1; + return 0; + +fail: + /* Error */ + wpa_printf(MSG_ERROR, "WPS UPnP: ssdp_listener_start failed"); + ssdp_listener_stop(sm); + return -1; +} + + +/** + * add_ssdp_network - Add routing entry for SSDP + * @net_if: Selected network interface name + * Returns: 0 on success, -1 on failure + * + * This function assures that the multicast address will be properly + * handled by Linux networking code (by a modification to routing tables). + * This must be done per network interface. It really only needs to be done + * once after booting up, but it does not hurt to call this more frequently + * "to be safe". + */ +int add_ssdp_network(char *net_if) +{ + int ret = -1; + int sock = -1; + struct rtentry rt; + struct sockaddr_in *sin; + + if (!net_if) + goto fail; + + os_memset(&rt, 0, sizeof(rt)); + sock = socket(AF_INET, SOCK_DGRAM, 0); + if (sock < 0) + goto fail; + + rt.rt_dev = net_if; + sin = (struct sockaddr_in *) &rt.rt_dst; + sin->sin_family = AF_INET; + sin->sin_port = 0; + sin->sin_addr.s_addr = inet_addr(SSDP_TARGET); + sin = (struct sockaddr_in *) &rt.rt_genmask; + sin->sin_family = AF_INET; + sin->sin_port = 0; + sin->sin_addr.s_addr = inet_addr(SSDP_NETMASK); + rt.rt_flags = RTF_UP; + if (ioctl(sock, SIOCADDRT, &rt) < 0) { + if (errno == EPERM) { + wpa_printf(MSG_DEBUG, "add_ssdp_network: No " + "permissions to add routing table entry"); + /* Continue to allow testing as non-root */ + } else if (errno != EEXIST) { + wpa_printf(MSG_INFO, "add_ssdp_network() ioctl errno " + "%d (%s)", errno, strerror(errno)); + goto fail; + } + } + + ret = 0; + +fail: + if (sock >= 0) + close(sock); + + return ret; +} + + +/** + * ssdp_open_multicast - Open socket for sending multicast SSDP messages + * @sm: WPS UPnP state machine from upnp_wps_device_init() + * Returns: 0 on success, -1 on failure + */ +int ssdp_open_multicast(struct upnp_wps_device_sm *sm) +{ + int sd = -1; + /* per UPnP-arch-DeviceArchitecture, 1. Discovery, keep IP packet + * time to live (TTL) small */ + unsigned char ttl = 4; + + sm->multicast_sd = sd = socket(AF_INET, SOCK_DGRAM, 0); + if (sd < 0) + return -1; + +#if 0 /* maybe ok if we sometimes block on writes */ + if (fcntl(sd, F_SETFL, O_NONBLOCK) != 0) + return -1; +#endif + + if (setsockopt(sd, IPPROTO_IP, IP_MULTICAST_IF, + &sm->ip_addr, sizeof(sm->ip_addr))) + return -1; + if (setsockopt(sd, IPPROTO_IP, IP_MULTICAST_TTL, + &ttl, sizeof(ttl))) + return -1; + +#if 0 /* not needed, because we don't receive using multicast_sd */ + { + struct ip_mreq mreq; + mreq.imr_multiaddr.s_addr = inet_addr(UPNP_MULTICAST_ADDRESS); + mreq.imr_interface.s_addr = sm->ip_addr; + wpa_printf(MSG_DEBUG, "WPS UPnP: Multicast addr 0x%x if addr " + "0x%x", + mreq.imr_multiaddr.s_addr, + mreq.imr_interface.s_addr); + if (setsockopt(sd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, + sizeof(mreq))) { + wpa_printf(MSG_ERROR, + "WPS UPnP: setsockopt " + "IP_ADD_MEMBERSHIP errno %d (%s)", + errno, strerror(errno)); + return -1; + } + } +#endif /* not needed */ + + /* + * TODO: What about IP_MULTICAST_LOOP? It seems to be on by default? + * which aids debugging I suppose but isn't really necessary? + */ + + return 0; +} diff --git a/src/wps/wps_upnp_web.c b/src/wps/wps_upnp_web.c new file mode 100644 index 000000000..2e0f29691 --- /dev/null +++ b/src/wps/wps_upnp_web.c @@ -0,0 +1,1959 @@ +/* + * 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 + * + * See wps_upnp.c for more details on licensing and code history. + */ + +#include "includes.h" +#include + +#include "common.h" +#include "base64.h" +#include "eloop.h" +#include "uuid.h" +#include "httpread.h" +#include "wps_i.h" +#include "wps_upnp.h" +#include "wps_upnp_i.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"; + +/* + * Incoming web connections are recorded in this struct. + * A web connection is a TCP connection to us, the server; + * it is called a "web connection" because we use http and serve + * data that looks like web pages. + * State information is need to track the connection until we figure + * out what they want and what we want to do about it. + */ +struct web_connection { + /* double linked list */ + struct web_connection *next; + struct web_connection *prev; + struct upnp_wps_device_sm *sm; /* parent */ + int sd; /* socket to read from */ + int sd_registered; /* nonzero if we must cancel registration */ + struct httpread *hread; /* state machine for reading socket */ + int n_rcvd_data; /* how much data read so far */ + int done; /* internal flag, set when we've finished */ +}; + + +/* + * XML parsing and formatting + * + * XML is a markup language based on unicode; usually (and in our case, + * always!) based on utf-8. utf-8 uses a variable number of bytes per + * character. utf-8 has the advantage that all non-ASCII unicode characters are + * represented by sequences of non-ascii (high bit set) bytes, whereas ASCII + * characters are single ascii bytes, thus we can use typical text processing. + * + * (One other interesting thing about utf-8 is that it is possible to look at + * any random byte and determine if it is the first byte of a character as + * versus a continuation byte). + * + * The base syntax of XML uses a few ASCII punctionation characters; any + * characters that would appear in the payload data are rewritten using + * sequences, e.g., & for ampersand(&) and < for left angle bracket (<). + * Five such escapes total (more can be defined but that does not apply to our + * case). Thus we can safely parse for angle brackets etc. + * + * XML describes tree structures of tagged data, with each element beginning + * with an opening tag with + * matching label. (There is also a self-closing tag