diff --git a/CMakeLists.txt b/CMakeLists.txt index 2cab9b3e3..1dd4cbcce 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1178,6 +1178,7 @@ if (LWS_WITH_NETWORK) lib/secure-streams/secure-streams.c lib/secure-streams/policy.c lib/secure-streams/system/fetch-policy/fetch-policy.c + lib/secure-streams/system/captive-portal-detect/captive-portal-detect.c ) if (LWS_ROLE_H1) list(APPEND SOURCES @@ -1218,6 +1219,7 @@ if (LWS_WITH_NETWORK) lib/secure-streams/system/auth-api.amazon.com/auth.c ) endif() + endif() if (LWS_WITH_STATS) diff --git a/READMEs/README.captive-portal-detection.md b/READMEs/README.captive-portal-detection.md new file mode 100644 index 000000000..ceab7b421 --- /dev/null +++ b/READMEs/README.captive-portal-detection.md @@ -0,0 +1,88 @@ +# Captive Portal Detection + +## Background + +Wifi devices may face some interception of their connection to the +internet, it's very common for, eg, coffee shop wifi to present some +kind of login or other clickthrough before access to the Internet is +granted. Devices may need to understand that they are in this +situation, and there are several different techniques for trying to +gague it. + +Sequence-wise the device has been granted a DHCP lease and has been +configured with DNS, but the DNS may be wrongly resolving everything +to an address on the LAN or a portal on the net. + +Whether there is a captive portal active should be a sticky state for a given +connection if there is not going to be any attempt to login or pass the landing +page, it only needs checking for after DHCP acquisition then. If there will be +an attempt to satisfy the landing page, the test should be repeated after the +attempt. + +## Detection schemes + +The most popular detection scheme by numbers is Android's method, +which is to make an HTTP client GET to `http://connectivitycheck.android.com/generate_204` +and see if a 204 is coming back... if intercepted, typically there'll be a +3xx redirect to the portal, perhaps on https. Or, it may reply on http with +a 200 and show the portal directly... either way it won't deliver a 204 +like the real remote server does. + +Variations include expecting a 200 but with specific http body content, and +doing a DNS lookup for a static IP that the device knows; if it's resolved to +something else, it knows there's monkey business implying a captive portal. + +Other schemes involve https connections going out and detecting that the cert +of the server it's actually talking to doesn't check out, although this is +potentially ambiguous. + +Yet more methods are possible outside of tcp or http. + +## lws captive portal detect support + +lws provides a generic api to start captive portal detection... + +``` +LWS_EXTERN LWS_VISIBLE int +lws_system_cpd_start(struct lws_context *context); +``` + +and two states in `lws_system` states to trigger it from, either +`LWS_SYSTATE_CPD_PRE_TIME` which happens after DHCP acquisition but before +ntpclient and is suitable for non https-based scheme where the time doesn't +need to be known, or the alternative `LWS_SYSTATE_CPD_POST_TIME` state which +happens after ntpclient has completed and we know the time. + +The actual platform implementation is set using `lws_system_ops_t` function +pointer `captive_portal_detect_request`, ie + +``` + int (*captive_portal_detect_request)(struct lws_context *context); + /**< Check if we can go out on the internet cleanly, or if we are being + * redirected or intercepted by a captive portal. + * Start the check that proceeds asynchronously, and report the results + * by calling lws_captive_portal_detect_result() api + */ +``` + +User platform code can provide this to implement whatever scheme they want, when +it has arrived at a result, it can call the lws api `lws_system_cpd_result()` to +inform lws. If there isn't any captive portal, this will also try to advance the +system state towards OPERATIONAL. + +``` +/** + * lws_system_cpd_result() - report the result of the captive portal detection + * + * \param context: the lws_context + * \param result: one of the LWS_CPD_ constants representing captive portal state + * \param redirect_url: NULL, or the url we were redirected to if result is + * LWS_CPD_HTTP_REDIRECT + * + * Sets the context's captive portal detection state to result. User captive + * portal detection code would call this once it had a result from its test. + */ +LWS_EXTERN LWS_VISIBLE int +lws_system_cpd_result(struct lws_context *context, int result, const char *redirect_url); +``` + diff --git a/include/libwebsockets/lws-callbacks.h b/include/libwebsockets/lws-callbacks.h index 727fdca8c..50cb73857 100644 --- a/include/libwebsockets/lws-callbacks.h +++ b/include/libwebsockets/lws-callbacks.h @@ -390,6 +390,9 @@ enum lws_callback_reasons { * lws know by calling lws_client_http_body_pending(wsi, 0) */ + LWS_CALLBACK_CLIENT_HTTP_REDIRECT = 104, + /**< we're handling a 3xx redirect... return nonzero to hang up */ + LWS_CALLBACK_CLIENT_HTTP_BIND_PROTOCOL = 85, LWS_CALLBACK_CLIENT_HTTP_DROP_PROTOCOL = 76, diff --git a/include/libwebsockets/lws-secure-streams-policy.h b/include/libwebsockets/lws-secure-streams-policy.h index 7cf3dce4c..f64fd80d2 100644 --- a/include/libwebsockets/lws-secure-streams-policy.h +++ b/include/libwebsockets/lws-secure-streams-policy.h @@ -206,6 +206,9 @@ typedef struct lws_ss_policy { /* false = TEXT, true = BINARY */ } ws; } u; + + uint16_t resp_expect; + uint8_t fail_redirect:1; } http; struct { diff --git a/include/libwebsockets/lws-system.h b/include/libwebsockets/lws-system.h index 61ddf3bd1..1f4370685 100644 --- a/include/libwebsockets/lws-system.h +++ b/include/libwebsockets/lws-system.h @@ -98,9 +98,24 @@ typedef enum { /* keep system_state_names[] in sync in context.c */ * can operate normally */ LWS_SYSTATE_IFACE_COLDPLUG, /* existing net ifaces iterated */ LWS_SYSTATE_DHCP, /* at least one net iface configured */ + LWS_SYSTATE_CPD_PRE_TIME, /* Captive portal detect without valid + * time, good for non-https tests... if + * you care about it, implement and + * call lws_system_ops_t + * .captive_portal_detect_request() + * and move the state forward according + * to the result. */ LWS_SYSTATE_TIME_VALID, /* ntpclient ran, or hw time valid... * tls cannot work until we reach here */ + LWS_SYSTATE_CPD_POST_TIME, /* Captive portal detect after time was + * time, good for https tests... if + * you care about it, implement and + * call lws_system_ops_t + * .captive_portal_detect_request() + * and move the state forward according + * to the result. */ + LWS_SYSTATE_POLICY_VALID, /* user code knows how to operate... */ LWS_SYSTATE_REGISTERED, /* device has an identity... */ LWS_SYSTATE_AUTH1, /* identity used for main auth token */ @@ -114,6 +129,16 @@ typedef enum { /* keep system_state_names[] in sync in context.c */ * LWS_SYSTATE_POLICY_VALID */ } lws_system_states_t; +/* Captive Portal Detect -related */ + +typedef enum { + LWS_CPD_UNKNOWN = 0, /* test didn't happen ince last DHCP acq yet */ + LWS_CPD_INTERNET_OK, /* no captive portal: our CPD test passed OK, + * we can go out on the internet */ + LWS_CPD_CAPTIVE_PORTAL, /* we inferred we're behind a captive portal */ + LWS_CPD_NO_INTERNET, /* we couldn't touch anything */ +} lws_cpd_result_t; + typedef void (*lws_attach_cb_t)(struct lws_context *context, int tsi, void *opaque); struct lws_attach_item; @@ -138,6 +163,12 @@ typedef struct lws_system_ops { * __lws_system_attach() is provided to do the actual work inside the * system-specific locking. */ + int (*captive_portal_detect_request)(struct lws_context *context); + /**< Check if we can go out on the internet cleanly, or if we are being + * redirected or intercepted by a captive portal. + * Start the check that proceeds asynchronously, and report the results + * by calling lws_captive_portal_detect_result() api + */ } lws_system_ops_t; /** @@ -231,7 +262,7 @@ typedef int (*dhcpc_cb_t)(void *opaque, int af, uint8_t *ip, int ip_len); * Register a network interface as being managed by DHCP. lws will proceed to * try to acquire an IP. Requires LWS_WITH_SYS_DHCP_CLIENT at cmake. */ -int +LWS_EXTERN LWS_VISIBLE int lws_dhcpc_request(struct lws_context *c, const char *i, int af, dhcpc_cb_t cb, void *opaque); @@ -243,7 +274,7 @@ lws_dhcpc_request(struct lws_context *c, const char *i, int af, dhcpc_cb_t cb, * * Remove handling of the network interface from dhcp. */ -int +LWS_EXTERN LWS_VISIBLE int lws_dhcpc_remove(struct lws_context *context, const char *iface); /** @@ -255,5 +286,42 @@ lws_dhcpc_remove(struct lws_context *context, const char *iface); * Returns 1 if any network interface managed by dhcpc has reached the BOUND * state (has acquired an IP, gateway and DNS server), otherwise 0. */ -int +LWS_EXTERN LWS_VISIBLE int lws_dhcpc_status(struct lws_context *context, lws_sockaddr46 *sa46); + +/** + * lws_system_cpd_start() - helper to initiate captive portal detection + * + * \param context: the lws_context + * + * Resets the context's captive portal state to LWS_CPD_UNKNOWN and calls the + * lws_system_ops_t captive_portal_detect_request() implementation to begin + * testing the captive portal state. + */ +LWS_EXTERN LWS_VISIBLE int +lws_system_cpd_start(struct lws_context *context); + + +/** + * lws_system_cpd_set() - report the result of the captive portal detection + * + * \param context: the lws_context + * \param result: one of the LWS_CPD_ constants representing captive portal state + * + * Sets the context's captive portal detection state to result. User captive + * portal detection code would call this once it had a result from its test. + */ +LWS_EXTERN LWS_VISIBLE void +lws_system_cpd_set(struct lws_context *context, lws_cpd_result_t result); + + +/** + * lws_system_cpd_state_get() - returns the last tested captive portal state + * + * \param context: the lws_context + * + * Returns one of the LWS_CPD_ constants indicating the system's understanding + * of the current captive portal situation. + */ +LWS_EXTERN LWS_VISIBLE lws_cpd_result_t +lws_system_cpd_state_get(struct lws_context *context); diff --git a/lib/core-net/state.c b/lib/core-net/state.c index fd9242929..56424647a 100644 --- a/lib/core-net/state.c +++ b/lib/core-net/state.c @@ -119,6 +119,9 @@ lws_state_transition_steps(lws_state_manager_t *mgr, int target) char temp8[8]; #endif + if (mgr->state > target) + return 0; + while (!n && mgr->state != target) n = _lws_state_transition(mgr, mgr->state + 1); diff --git a/lib/core/context.c b/lib/core/context.c index 66eb625c4..ced00eb3a 100644 --- a/lib/core/context.c +++ b/lib/core/context.c @@ -82,7 +82,9 @@ static const char * system_state_names[] = { "INITIALIZED", "IFACE_COLDPLUG", "DHCP", + "CPD_PRE_TIME", "TIME_VALID", + "CPD_POST_TIME", "POLICY_VALID", "REGISTERED", "AUTH1", @@ -145,6 +147,20 @@ lws_state_notify_protocol_init(struct lws_state_manager *mgr, #endif #if defined(LWS_WITH_SECURE_STREAMS) + /* + * See if we should do the SS Captive Portal Detection + */ + if (target == LWS_SYSTATE_CPD_PRE_TIME) { + if (lws_system_cpd_state_get(context)) + return 0; /* allow it */ + + lwsl_info("%s: LWS_SYSTATE_CPD_PRE_TIME\n", __func__); + if (!lws_system_cpd_start(context)) + return 1; + + /* it failed, eg, no streamtype for it in the policy */ + } + /* * Skip this if we are running something without the policy for it */ @@ -896,10 +912,56 @@ fail_event_libs: return NULL; } +#if defined(LWS_WITH_NETWORK) int -lws_context_is_deprecated(struct lws_context *context) +lws_system_cpd_start(struct lws_context *cx) { - return context->deprecated; + cx->captive_portal_detect = LWS_CPD_UNKNOWN; + + /* if there's a platform implementation, use it */ + + if (lws_system_get_ops(cx) && + lws_system_get_ops(cx)->captive_portal_detect_request) + return lws_system_get_ops(cx)->captive_portal_detect_request(cx); + +#if defined(LWS_WITH_SECURE_STREAMS) + /* + * Otherwise try to use SS "captive_portal_detect" if that's enabled + */ + return lws_ss_sys_cpd(cx); +#else + return 0; +#endif +} + +static const char *cname[] = { "?", "OK", "Captive", "No internet" }; + +void +lws_system_cpd_set(struct lws_context *cx, lws_cpd_result_t result) +{ + if (cx->captive_portal_detect != LWS_CPD_UNKNOWN) + return; + + lwsl_notice("%s: setting CPD result %s\n", __func__, cname[result]); + + cx->captive_portal_detect = (uint8_t)result; + + /* if nothing is there to intercept anything, go all the way */ + lws_state_transition_steps(&cx->mgr_system, LWS_SYSTATE_OPERATIONAL); +} + +lws_cpd_result_t +lws_system_cpd_state_get(struct lws_context *cx) +{ + return (lws_cpd_result_t)cx->captive_portal_detect; +} + +#endif + +int +lws_context_is_deprecated(struct lws_context *cx) +{ + return cx->deprecated; } /* diff --git a/lib/core/private-lib-core.h b/lib/core/private-lib-core.h index c508e91f0..77380456b 100644 --- a/lib/core/private-lib-core.h +++ b/lib/core/private-lib-core.h @@ -523,6 +523,8 @@ struct lws_context { uint8_t max_fi; uint8_t udp_loss_sim_tx_pc; uint8_t udp_loss_sim_rx_pc; + uint8_t captive_portal_detect; + uint8_t captive_portal_detect_type; #if defined(LWS_WITH_STATS) uint8_t updated; diff --git a/lib/roles/http/client/client-http.c b/lib/roles/http/client/client-http.c index 76b788fb7..009a918a6 100644 --- a/lib/roles/http/client/client-http.c +++ b/lib/roles/http/client/client-http.c @@ -678,6 +678,15 @@ lws_client_interpret_server_handshake(struct lws *wsi) goto bail3; } + /* let's let the user code know, if he cares */ + + if (wsi->protocol->callback(wsi, + LWS_CALLBACK_CLIENT_HTTP_REDIRECT, + wsi->user_space, p, n)) { + cce = "HS: user code rejected redirect"; + goto bail3; + } + /* * Some redirect codes imply we have to change the method * used for the subsequent transaction, commonly POST -> diff --git a/lib/secure-streams/README.md b/lib/secure-streams/README.md index 8f815e054..59ccf319c 100644 --- a/lib/secure-streams/README.md +++ b/lib/secure-streams/README.md @@ -177,6 +177,23 @@ to validate the remote server cert. HTTP method to use with http-related protocols, like GET or POST. Not required for ws. +### `http_expect` + +Optionally indicates that success for HTTP transactions using this +streamtype is different than the default 200 - 299. + +Eg, you may choose to set this to 204 for Captive Portal Detect usage +if that's what you expect the server to reply with to indicate +success. In that case, anything other than 204 will be treated as a +connection failure. + +### `http_fail_redirect` + +Set to `true` if you want to fail the connection on meeting an +http redirect. This is needed to, eg, detect Captive Portals +correctly. Normally, if on https, you would want the default behaviour +of following the redirect. + ### `http_url` Url path to use with http-related protocols @@ -352,6 +369,26 @@ The secure-streams-proxy minimal example shows how this is done and fetches its real policy from warmcat.com at startup using the built-in one. +## Captive Portal Detection + +If the policy contains a streamtype `captive_portal_detect` then the +type of transaction described there is automatically performed after +acquiring a DHCP address to try to determine the captive portal +situation. + +``` + "captive_portal_detect": { + "endpoint": "connectivitycheck.android.com", + "port": 80, + "protocol": "h1", + "http_method": "GET", + "http_url": "generate_204", + "opportunistic": true, + "http_expect": 204, + "http_fail_redirect": true + } +``` + ## Stream serialization and proxying By default Secure Streams expects to make the outgoing connection described in diff --git a/lib/secure-streams/policy.c b/lib/secure-streams/policy.c index ec59e7fce..d35c003cf 100644 --- a/lib/secure-streams/policy.c +++ b/lib/secure-streams/policy.c @@ -79,6 +79,8 @@ static const char * const lejp_tokens_policy[] = { "s[].*.http_multipart_filename", "s[].*.http_mime_content_type", "s[].*.http_www_form_urlencoded", + "s[].*.http_expect", + "s[].*.http_fail_redirect", "s[].*.ws_subprotocol", "s[].*.ws_binary", "s[].*.local_sink", @@ -142,6 +144,8 @@ typedef enum { LSSPPT_HTTP_MULTIPART_FILENAME, LSSPPT_HTTP_MULTIPART_CONTENT_TYPE, LSSPPT_HTTP_WWW_FORM_URLENCODED, + LSSPPT_HTTP_EXPECT, + LSSPPT_HTTP_FAIL_REDIRECT, LSSPPT_WS_SUBPROTOCOL, LSSPPT_WS_BINARY, LSSPPT_LOCAL_SINK, @@ -524,6 +528,10 @@ lws_ss_policy_parser_cb(struct lejp_ctx *ctx, char reason) a->curr[LTY_POLICY].p->client_cert = atoi(ctx->buf) + 1; break; + case LSSPPT_HTTP_EXPECT: + a->curr[LTY_POLICY].p->u.http.resp_expect = atoi(ctx->buf); + break; + case LSSPPT_OPPORTUNISTIC: if (reason == LEJPCB_VAL_TRUE) a->curr[LTY_POLICY].p->flags |= LWSSSPOLF_OPPORTUNISTIC; @@ -644,6 +652,12 @@ lws_ss_policy_parser_cb(struct lejp_ctx *ctx, char reason) a->curr[LTY_POLICY].p->flags |= LWSSSPOLF_HTTP_MULTIPART; pp = (char **)&a->curr[LTY_POLICY].p->u.http.multipart_content_type; goto string2; + + case LSSPPT_HTTP_FAIL_REDIRECT: + a->curr[LTY_POLICY].p->u.http.fail_redirect = + reason == LEJPCB_VAL_TRUE; + break; + case LSSPPT_WS_SUBPROTOCOL: pp = (char **)&a->curr[LTY_POLICY].p->u.http.u.ws.subprotocol; goto string2; diff --git a/lib/secure-streams/private-lib-secure-streams.h b/lib/secure-streams/private-lib-secure-streams.h index b22211a31..fc8b88e84 100644 --- a/lib/secure-streams/private-lib-secure-streams.h +++ b/lib/secure-streams/private-lib-secure-streams.h @@ -318,6 +318,9 @@ int lws_ss_exp_cb_metadata(void *priv, const char *name, char *out, size_t *pos, size_t olen, size_t *exp_ofs); +int +lws_ss_sys_cpd(struct lws_context *cx); + typedef int (* const secstream_protocol_connect_munge_t)(lws_ss_handle_t *h, char *buf, size_t len, struct lws_client_connect_info *i, union lws_ss_contemp *ct); diff --git a/lib/secure-streams/protocols/ss-h1.c b/lib/secure-streams/protocols/ss-h1.c index c9c6185cc..1de0868f3 100644 --- a/lib/secure-streams/protocols/ss-h1.c +++ b/lib/secure-streams/protocols/ss-h1.c @@ -167,7 +167,6 @@ secstream_h1(struct lws *wsi, enum lws_callback_reasons reason, void *user, switch (reason) { - /* because we are protocols[0] ... */ case LWS_CALLBACK_CLIENT_CONNECTION_ERROR: assert(h); assert(h->policy); @@ -178,6 +177,13 @@ secstream_h1(struct lws *wsi, enum lws_callback_reasons reason, void *user, lws_ss_backoff(h); break; + case LWS_CALLBACK_CLIENT_HTTP_REDIRECT: + if (h->policy->u.http.fail_redirect) + lws_system_cpd_set(lws_get_context(wsi), + LWS_CPD_CAPTIVE_PORTAL); + /* don't follow it */ + return 1; + case LWS_CALLBACK_CLOSED_CLIENT_HTTP: if (!h) break; @@ -201,7 +207,12 @@ secstream_h1(struct lws *wsi, enum lws_callback_reasons reason, void *user, // if (!status) /* it's just telling use we connected / joined the nwsi */ // break; - h->u.http.good_respcode = (status >= 200 && status < 300); + + if (h->policy->u.http.resp_expect) + h->u.http.good_respcode = + status == h->policy->u.http.resp_expect; + else + h->u.http.good_respcode = (status >= 200 && status < 300); // lwsl_err("%s: good resp %d %d\n", __func__, status, h->u.http.good_respcode); if (h->u.http.good_respcode) diff --git a/lib/secure-streams/secure-streams.c b/lib/secure-streams/secure-streams.c index 652f6c9a7..b5eb40334 100644 --- a/lib/secure-streams/secure-streams.c +++ b/lib/secure-streams/secure-streams.c @@ -302,8 +302,8 @@ lws_ss_create(struct lws_context *context, int tsi, const lws_ss_info_t *ssi, pol = lws_ss_policy_lookup(context, ssi->streamtype); if (!pol) { - lwsl_err("%s: unknown stream type %s\n", __func__, - ssi->streamtype); + lwsl_notice("%s: unknown stream type %s\n", __func__, + ssi->streamtype); return 1; } diff --git a/lib/secure-streams/system/captive-portal-detect/captive-portal-detect.c b/lib/secure-streams/system/captive-portal-detect/captive-portal-detect.c new file mode 100644 index 000000000..82b749491 --- /dev/null +++ b/lib/secure-streams/system/captive-portal-detect/captive-portal-detect.c @@ -0,0 +1,111 @@ +/* + * Captive portal detect for Secure Streams + * + * libwebsockets - small server side websockets and web server implementation + * + * Copyright (C) 2019 - 2020 Andy Green + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +#include + +typedef struct ss_cpd { + struct lws_ss_handle *ss; + void *opaque_data; + /* ... application specific state ... */ + + lws_sorted_usec_list_t sul; + + uint8_t partway; +} ss_cpd_t; + +/* secure streams payload interface */ + +static int +ss_cpd_rx(void *userobj, const uint8_t *buf, size_t len, int flags) +{ + return 0; +} + +static int +ss_cpd_tx(void *userobj, lws_ss_tx_ordinal_t ord, uint8_t *buf, + size_t *len, int *flags) +{ + return 1; +} + +static int +ss_cpd_state(void *userobj, void *sh, lws_ss_constate_t state, + lws_ss_tx_ordinal_t ack) +{ + ss_cpd_t *m = (ss_cpd_t *)userobj; + struct lws_context *cx = (struct lws_context *)m->opaque_data; + + lwsl_info("%s: %s, ord 0x%x\n", __func__, lws_ss_state_name(state), + (unsigned int)ack); + + switch (state) { + case LWSSSCS_CREATING: + lws_ss_request_tx(m->ss); + break; + case LWSSSCS_QOS_ACK_REMOTE: + lws_system_cpd_set(cx, LWS_CPD_INTERNET_OK); + break; + + case LWSSSCS_ALL_RETRIES_FAILED: + case LWSSSCS_DISCONNECTED: + /* + * First result reported sticks... if nothing else, this will + * cover the situation we didn't connect to anything + */ + lws_system_cpd_set(cx, LWS_CPD_NO_INTERNET); + break; + + default: + break; + } + + return 0; +} + +int +lws_ss_sys_cpd(struct lws_context *cx) +{ + lws_ss_info_t ssi; + + /* We're making an outgoing secure stream ourselves */ + + memset(&ssi, 0, sizeof(ssi)); + ssi.handle_offset = offsetof(ss_cpd_t, ss); + ssi.opaque_user_data_offset = offsetof(ss_cpd_t, opaque_data); + ssi.rx = ss_cpd_rx; + ssi.tx = ss_cpd_tx; + ssi.state = ss_cpd_state; + ssi.user_alloc = sizeof(ss_cpd_t); + ssi.streamtype = "captive_portal_detect"; + + if (lws_ss_create(cx, 0, &ssi, cx, NULL, NULL, NULL)) { + lwsl_info("%s: Create stream failed (policy?)\n", __func__); + + return 1; + } + + return 0; +} diff --git a/minimal-examples/http-client/minimal-http-client-captive-portal/CMakeLists.txt b/minimal-examples/http-client/minimal-http-client-captive-portal/CMakeLists.txt new file mode 100644 index 000000000..b9d34fdc1 --- /dev/null +++ b/minimal-examples/http-client/minimal-http-client-captive-portal/CMakeLists.txt @@ -0,0 +1,82 @@ +project(lws-minimal-http-client-captive-portal) +cmake_minimum_required(VERSION 2.8) +include(CheckIncludeFile) +include(CheckCSourceCompiles) + +set(SAMP lws-minimal-http-client-captive-portal) +set(SRCS minimal-http-client-captive-portal.c) + +# If we are being built as part of lws, confirm current build config supports +# reqconfig, else skip building ourselves. +# +# If we are being built externally, confirm installed lws was configured to +# support reqconfig, else error out with a helpful message about the problem. +# +MACRO(require_lws_config reqconfig _val result) + + if (DEFINED ${reqconfig}) + if (${reqconfig}) + set (rq 1) + else() + set (rq 0) + endif() + else() + set(rq 0) + endif() + + if (${_val} EQUAL ${rq}) + set(SAME 1) + else() + set(SAME 0) + endif() + + if (LWS_WITH_MINIMAL_EXAMPLES AND NOT ${SAME}) + if (${_val}) + message("${SAMP}: skipping as lws being built without ${reqconfig}") + else() + message("${SAMP}: skipping as lws built with ${reqconfig}") + endif() + set(${result} 0) + else() + if (LWS_WITH_MINIMAL_EXAMPLES) + set(MET ${SAME}) + else() + CHECK_C_SOURCE_COMPILES("#include \nint main(void) {\n#if defined(${reqconfig})\n return 0;\n#else\n fail;\n#endif\n return 0;\n}\n" HAS_${reqconfig}) + if (NOT DEFINED HAS_${reqconfig} OR NOT HAS_${reqconfig}) + set(HAS_${reqconfig} 0) + else() + set(HAS_${reqconfig} 1) + endif() + if ((HAS_${reqconfig} AND ${_val}) OR (NOT HAS_${reqconfig} AND NOT ${_val})) + set(MET 1) + else() + set(MET 0) + endif() + endif() + if (NOT MET) + if (${_val}) + message(FATAL_ERROR "This project requires lws must have been configured with ${reqconfig}") + else() + message(FATAL_ERROR "Lws configuration of ${reqconfig} is incompatible with this project") + endif() + endif() + endif() +ENDMACRO() + +set(requirements 1) +if (WIN32) + set(requirements 0) +endif() +require_lws_config(LWS_ROLE_H1 1 requirements) +require_lws_config(LWS_WITH_CLIENT 1 requirements) + +if (requirements) + add_executable(${SAMP} ${SRCS}) + + if (websockets_shared) + target_link_libraries(${SAMP} websockets_shared pthread) + add_dependencies(${SAMP} websockets_shared) + else() + target_link_libraries(${SAMP} websockets pthread) + endif() +endif() diff --git a/minimal-examples/http-client/minimal-http-client-captive-portal/README.md b/minimal-examples/http-client/minimal-http-client-captive-portal/README.md new file mode 100644 index 000000000..8db20026a --- /dev/null +++ b/minimal-examples/http-client/minimal-http-client-captive-portal/README.md @@ -0,0 +1,45 @@ +# lws minimal http client captive portal detect + +This demonstrates how to perform captive portal detection integrated +with `lws_system` states. + +After reaching the `lws_system` DHCP state, the application tries to +connect through to `http://connectivitycheck.android.com/generate_204` +over http... if it succeeds, it will get a 204 response and set the +captive portal detection state to `LWS_CPD_INTERNET_OK` and perform +a GET from warmcat.com. + +If there is a problem detected, the captive portal detection state is +set accordingly and the app will respond by exiting without trying the +read from warmcat.com. + +The captive portal detection scheme is implemented in the user code +and can be modified according to the strategy that's desired for +captive portal detection. + +## build + +``` + $ cmake . && make +``` + +## usage + +``` +$ ./bin/lws-minimal-http-client-captive-portal +[2020/03/11 13:07:07:4519] U: LWS minimal http client captive portal detect +[2020/03/11 13:07:07:4519] N: lws_create_context: using ss proxy bind '(null)', port 0, ads '(null)' +[2020/03/11 13:07:07:5022] U: callback_cpd_http: established with resp 204 +[2020/03/11 13:07:07:5023] U: app_system_state_nf: OPERATIONAL, cpd 1 +[2020/03/11 13:07:07:5896] U: Connected to 46.105.127.147, http response: 200 +[2020/03/11 13:07:07:5931] U: RECEIVE_CLIENT_HTTP_READ: read 4087 +[2020/03/11 13:07:07:5931] U: RECEIVE_CLIENT_HTTP_READ: read 4096 +[2020/03/11 13:07:07:6092] U: RECEIVE_CLIENT_HTTP_READ: read 4087 +[2020/03/11 13:07:07:6092] U: RECEIVE_CLIENT_HTTP_READ: read 4096 +[2020/03/11 13:07:07:6112] U: RECEIVE_CLIENT_HTTP_READ: read 4087 +[2020/03/11 13:07:07:6113] U: RECEIVE_CLIENT_HTTP_READ: read 4096 +[2020/03/11 13:07:07:6113] U: RECEIVE_CLIENT_HTTP_READ: read 2657 +[2020/03/11 13:07:07:6113] U: LWS_CALLBACK_COMPLETED_CLIENT_HTTP +[2020/03/11 13:07:07:6119] U: main: finished OK +``` + diff --git a/minimal-examples/http-client/minimal-http-client-captive-portal/minimal-http-client-captive-portal.c b/minimal-examples/http-client/minimal-http-client-captive-portal/minimal-http-client-captive-portal.c new file mode 100644 index 000000000..38d1a7668 --- /dev/null +++ b/minimal-examples/http-client/minimal-http-client-captive-portal/minimal-http-client-captive-portal.c @@ -0,0 +1,321 @@ +/* + * lws-minimal-http-client-captive-portal + * + * Written in 2010-2020 by Andy Green + * + * This file is made available under the Creative Commons CC0 1.0 + * Universal Public Domain Dedication. + * + * This demonstrates how to use the lws_system captive portal detect integration + * + * We check for a captive portal by doing a GET from + * http://connectivitycheck.android.com/generate_204, if we really are going + * out on the Internet he'll return with a 204 response code and we will + * understand there's no captive portal. If we get something else, we take it + * there is a captive portal. + */ + +#include +#include +#include + +static struct lws_context *context; +static int interrupted, bad = 1, status; +static lws_state_notify_link_t nl; + +/* + * this is the user code http handler + */ + +static int +callback_http(struct lws *wsi, enum lws_callback_reasons reason, + void *user, void *in, size_t len) +{ + switch (reason) { + + /* because we are protocols[0] ... */ + case LWS_CALLBACK_CLIENT_CONNECTION_ERROR: + lwsl_err("CLIENT_CONNECTION_ERROR: %s\n", + in ? (char *)in : "(null)"); + interrupted = 1; + break; + + case LWS_CALLBACK_ESTABLISHED_CLIENT_HTTP: + { + char buf[128]; + + lws_get_peer_simple(wsi, buf, sizeof(buf)); + status = lws_http_client_http_response(wsi); + + lwsl_user("Connected to %s, http response: %d\n", + buf, status); + } + break; + + /* chunks of chunked content, with header removed */ + case LWS_CALLBACK_RECEIVE_CLIENT_HTTP_READ: + lwsl_user("RECEIVE_CLIENT_HTTP_READ: read %d\n", (int)len); + +#if 0 /* enable to dump the html */ + { + const char *p = in; + + while (len--) + if (*p < 0x7f) + putchar(*p++); + else + putchar('.'); + } +#endif + return 0; /* don't passthru */ + + /* uninterpreted http content */ + case LWS_CALLBACK_RECEIVE_CLIENT_HTTP: + { + char buffer[1024 + LWS_PRE]; + char *px = buffer + LWS_PRE; + int lenx = sizeof(buffer) - LWS_PRE; + + if (lws_http_client_read(wsi, &px, &lenx) < 0) + return -1; + } + return 0; /* don't passthru */ + + case LWS_CALLBACK_COMPLETED_CLIENT_HTTP: + lwsl_user("LWS_CALLBACK_COMPLETED_CLIENT_HTTP\n"); + interrupted = 1; + bad = status != 200; + lws_cancel_service(lws_get_context(wsi)); /* abort poll wait */ + break; + + case LWS_CALLBACK_CLOSED_CLIENT_HTTP: + interrupted = 1; + bad = status != 200; + lws_cancel_service(lws_get_context(wsi)); /* abort poll wait */ + break; + + default: + break; + } + + return lws_callback_http_dummy(wsi, reason, user, in, len); +} + +/* + * This is the platform's custom captive portal detection handler + */ + +static int +callback_cpd_http(struct lws *wsi, enum lws_callback_reasons reason, + void *user, void *in, size_t len) +{ + int resp; + + switch (reason) { + + case LWS_CALLBACK_ESTABLISHED_CLIENT_HTTP: + resp = lws_http_client_http_response(wsi); + if (!resp) + break; + lwsl_user("%s: established with resp %d\n", __func__, resp); + switch (resp) { + + case HTTP_STATUS_NO_CONTENT: + /* + * We got the 204 which is used to distinguish the real + * endpoint + */ + lws_system_cpd_set(lws_get_context(wsi), + LWS_CPD_INTERNET_OK); + return 0; + + /* also case HTTP_STATUS_OK: ... */ + default: + break; + } + + /* fallthru */ + + case LWS_CALLBACK_CLIENT_HTTP_REDIRECT: + lws_system_cpd_set(lws_get_context(wsi), LWS_CPD_CAPTIVE_PORTAL); + /* don't follow it, just report it */ + return 1; + + case LWS_CALLBACK_CLIENT_CONNECTION_ERROR: + case LWS_CALLBACK_CLOSED_CLIENT_HTTP: + /* only the first result counts */ + lws_system_cpd_set(lws_get_context(wsi), LWS_CPD_NO_INTERNET); + break; + + default: + break; + } + + return lws_callback_http_dummy(wsi, reason, user, in, len); +} + +static const struct lws_protocols protocols[] = { + { + "http", + callback_http, + 0, + 0, + }, { + "lws-cpd-http", + callback_cpd_http + }, + { NULL, NULL, 0, 0 } +}; + +void sigint_handler(int sig) +{ + interrupted = 1; +} + +/* + * This triggers our platform implementation of captive portal detection, the + * actual test can be whatever you need. + * + * In this example, we detect it using Android's + * + * http://connectivitycheck.android.com/generate_204 + * + * and seeing if we get an http 204 back. + */ + +static int +captive_portal_detect_request(struct lws_context *context) +{ + struct lws_client_connect_info i; + + memset(&i, 0, sizeof i); + i.context = context; + i.port = 80; + i.address = "connectivitycheck.android.com"; + i.path = "/generate_204"; + i.host = i.address; + i.origin = i.address; + i.method = "GET"; + + i.protocol = "lws-cpd-http"; + + return !lws_client_connect_via_info(&i); +} + + +lws_system_ops_t ops = { + .captive_portal_detect_request = captive_portal_detect_request +}; + + +static int +app_system_state_nf(lws_state_manager_t *mgr, lws_state_notify_link_t *link, + int current, int target) +{ + struct lws_context *cx = lws_system_context_from_system_mgr(mgr); + + switch (target) { + case LWS_SYSTATE_CPD_PRE_TIME: + if (lws_system_cpd_state_get(cx)) + return 0; /* allow it */ + + lwsl_info("%s: LWS_SYSTATE_CPD_PRE_TIME\n", __func__); + lws_system_cpd_start(cx); + /* we'll move the state on when we get a result */ + return 1; + + case LWS_SYSTATE_OPERATIONAL: + if (current == LWS_SYSTATE_OPERATIONAL) { + struct lws_client_connect_info i; + + lwsl_user("%s: OPERATIONAL, cpd %d\n", __func__, + lws_system_cpd_state_get(cx)); + + /* + * When we reach the OPERATIONAL lws_system state, we + * can do our main job knowing we have DHCP, ntpclient, + * captive portal testing done. + */ + + if (lws_system_cpd_state_get(cx) != LWS_CPD_INTERNET_OK) { + lwsl_warn("%s: There's no internet...\n", __func__); + interrupted = 1; + break; + } + + memset(&i, 0, sizeof i); + i.context = context; + i.ssl_connection = LCCSCF_USE_SSL; + i.ssl_connection |= LCCSCF_H2_QUIRK_OVERFLOWS_TXCR | + LCCSCF_H2_QUIRK_NGHTTP2_END_STREAM; + i.port = 443; + i.address = "warmcat.com"; + i.path = "/"; + i.host = i.address; + i.origin = i.address; + i.method = "GET"; + + i.protocol = protocols[0].name; + + lws_client_connect_via_info(&i); + break; + } + default: + break; + } + + return 0; +} + +static lws_state_notify_link_t * const app_notifier_list[] = { + &nl, NULL +}; + +/* + * We made this into a different thread to model it being run from completely + * different codebase that's all linked together + */ + + +int main(int argc, const char **argv) +{ + int logs = LLL_USER | LLL_ERR | LLL_WARN | LLL_NOTICE; + struct lws_context_creation_info info; + const char *p; + + signal(SIGINT, sigint_handler); + + if ((p = lws_cmdline_option(argc, argv, "-d"))) + logs = atoi(p); + + lws_set_log_level(logs, NULL); + lwsl_user("LWS minimal http client captive portal detect\n"); + + memset(&info, 0, sizeof info); + info.port = CONTEXT_PORT_NO_LISTEN; + info.options = LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT; + info.system_ops = &ops; + info.protocols = protocols; + + /* integrate us with lws system state management when context created */ + + nl.name = "app"; + nl.notify_cb = app_system_state_nf; + info.register_notifier_list = app_notifier_list; + + context = lws_create_context(&info); + if (!context) { + lwsl_err("lws init failed\n"); + return 1; + } + + while (!interrupted) + if (lws_service(context, 0)) + interrupted = 1; + + lws_context_destroy(context); + + lwsl_user("%s: finished %s\n", __func__, bad ? "FAIL": "OK"); + + return bad; +} diff --git a/minimal-examples/secure-streams/minimal-secure-streams/minimal-secure-streams.c b/minimal-examples/secure-streams/minimal-secure-streams/minimal-secure-streams.c index fe44a437a..2c5a27869 100644 --- a/minimal-examples/secure-streams/minimal-secure-streams/minimal-secure-streams.c +++ b/minimal-examples/secure-streams/minimal-secure-streams/minimal-secure-streams.c @@ -154,6 +154,11 @@ static const char * const default_ss_policy = "}" "]," "\"s\": [" + /* + * "fetch_policy" decides from where the real policy + * will be fetched, if present. Otherwise the initial + * policy is treated as the whole, hardcoded, policy. + */ "{\"fetch_policy\": {" "\"endpoint\":" "\"warmcat.com\"," "\"port\":" "443," @@ -168,8 +173,42 @@ static const char * const default_ss_policy = "\"opportunistic\":" "true," "\"retry\":" "\"default\"," "\"tls_trust_store\":" "\"le_via_isrg\"" - "}}" - "}" + "}},{" + /* + * "captive_portal_detect" describes + * what to do in order to check if the path to + * the Internet is being interrupted by a + * captive portal. If there's a larger policy + * fetched from elsewhere, it should also include + * this since it needs to be done at least after + * every DHCP acquisition + */ + "\"captive_portal_detect\": {" +#if 1 + /* this does the actual test */ + "\"endpoint\": \"connectivitycheck.android.com\"," + "\"http_url\": \"generate_204\"," + "\"port\": 80," +#endif +#if 0 + /* this looks like a captive portal due to redirect */ + "\"endpoint\": \"google.com\"," + "\"http_url\": \"/\"," + "\"port\": 80," +#endif +#if 0 + /* this looks like no internet */ + "\"endpoint\": \"warmcat.com\"," + "\"http_url\": \"/\"," + "\"port\": 999," +#endif + "\"protocol\": \"h1\"," + "\"http_method\": \"GET\"," + "\"opportunistic\": true," + "\"http_expect\": 204," + "\"http_fail_redirect\": true" + "}}" + "]}" ; #endif