1
0
Fork 0
mirror of https://github.com/warmcat/libwebsockets.git synced 2025-03-09 00:00:04 +01:00

captive portal

Implement Captive Portal detection support in lws, with the actual
detection happening in platform code hooked up by lws_system_ops_t.

Add an implementation using Secure Streams as well, if the policy
defines captive_portal_detect streamtype, a SS using that streamtype
is used to probe if it's behind a captive portal.
This commit is contained in:
Andy Green 2020-03-11 12:44:01 +00:00
parent fdb9b7fdd3
commit a60cb84c9e
19 changed files with 914 additions and 11 deletions

View file

@ -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)

View file

@ -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);
```

View file

@ -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,

View file

@ -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 {

View file

@ -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);

View file

@ -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);

View file

@ -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;
}
/*

View file

@ -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;

View file

@ -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 ->

View file

@ -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

View file

@ -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;

View file

@ -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);

View file

@ -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)

View file

@ -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;
}

View file

@ -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 <andy@warmcat.com>
*
* 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 <private-lib-core.h>
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;
}

View file

@ -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 <libwebsockets.h>\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()

View file

@ -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
```

View file

@ -0,0 +1,321 @@
/*
* lws-minimal-http-client-captive-portal
*
* Written in 2010-2020 by Andy Green <andy@warmcat.com>
*
* 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 <libwebsockets.h>
#include <string.h>
#include <signal.h>
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;
}

View file

@ -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