blob: df69b830802b018b06b232ab37996ca6cc755b5e [file] [log] [blame]
/*
* Portal Check - Determines if a user is in an IP restrict pool or behind a
* captive portal.
*
* This file initially created by Google, Inc.
*
* 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.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <stdlib.h>
#include <time.h>
#include <curl/curl.h>
#include "connman.h"
#define lengthof(array) (sizeof (array) / sizeof ((array)[0]))
#define _DBG_PORTAL(fmt, arg...) DBG(DBG_PORTAL, fmt, ## arg)
#define MSEC_PER_SEC 1000LL
#define NSEC_PER_SEC 1000000000LL
#define REQUEST_TIMEOUT_S 10
static CURLM *curl_multi_handle_;
struct curl_source {
GSource source; /* base class */
guint fd_bmap;
GPollFD poll_fds[sizeof(guint) * 8];
struct timespec expiration;
};
static struct curl_source *curl_source;
struct portal_request {
struct connman_service *service;
struct connman_device *device;
char *interface;
CURL *easy_handle;
connman_bool_t handle_added;
};
static GSList *request_list = NULL;
/**
* Allocate a new portal request and make it the latest request
*/
static struct portal_request *new_request(struct connman_service *service,
struct connman_device *device,
const char *interface,
CURL *easy_handle)
{
struct portal_request *request = g_try_new0(struct portal_request, 1);
if (request == NULL)
return NULL;
request->service = connman_service_ref(service);
request->device = connman_device_ref(device);
request->interface = g_strdup(interface);
request->easy_handle = easy_handle;
request->handle_added = FALSE;
connman_device_rp_filter_disable(device);
request_list = g_slist_append(request_list, request);
return request;
}
/**
* Free a portal request
*/
static void free_request(struct portal_request *request)
{
request_list = g_slist_remove(request_list, request);
if (request->handle_added)
curl_multi_remove_handle(curl_multi_handle_,
request->easy_handle);
curl_easy_cleanup(request->easy_handle);
connman_device_rp_filter_enable(request->device);
g_free(request->interface);
connman_device_unref(request->device);
connman_service_unref(request->service);
g_free(request);
}
/**
* Cancels a request
*/
static void cancel_request(struct portal_request *request)
{
_DBG_PORTAL("%s: cancelling old request", request->interface);
free_request(request);
}
/**
* Cancels old requests on this request's interface
*/
static void cancel_old_requests(struct portal_request *request)
{
GSList *list;
list = request_list;
while(list != NULL) {
struct portal_request *old_request = list->data;
list = list->next;
if (old_request == request)
continue;
if (request->service == old_request->service)
cancel_request(old_request);
}
}
/**
* Cancels old requests on this service
*/
static void cancel_service_requests(struct connman_service *service)
{
GSList *list;
for (list = request_list; list != NULL; list = list->next) {
struct portal_request *request = list->data;
if (request->service == service)
cancel_request(request);
}
}
/**
* Return TRUE if there is a request in progress for this service.
*/
static connman_bool_t service_has_request(struct connman_service *service)
{
GSList *list;
for (list = request_list; list != NULL; list = list->next) {
struct portal_request *request = list->data;
if (request->service == service)
return TRUE;
}
return FALSE;
}
/**
* Returns the HTTP response code from a completed curl request.
*/
static long get_http_code(CURL *curl_handle)
{
long http_code = 0;
curl_easy_getinfo (curl_handle, CURLINFO_RESPONSE_CODE, &http_code);
_DBG_PORTAL("http response code is %d", (int) http_code);
return http_code;
}
/**
* Returns a connectivity state based on the status of an HTTP request
* to portal check URL
*/
static enum connman_service_connectivity_state check_connectivity_state(
CURLMsg *request_status,
const char *interface)
{
CURLcode code = (CURLcode)request_status->data.result;
/* Check the request state */
if (CURLE_COULDNT_RESOLVE_HOST == code) {
/*
* TODO(jglasgow): test resolving the address of the
* captive portal web server instead
*/
connman_info("%s: cannot resolve %s, marking portal",
interface,
__connman_profile_get_portal_url());
return CONNMAN_SERVICE_CONNECTIVITY_STATE_NONE;
}
if (CURLE_OK == code) {
int http_status_code = get_http_code(request_status->easy_handle);
if (204 == http_status_code) {
connman_info("%s: interface can route traffic, marking online",
interface);
return CONNMAN_SERVICE_CONNECTIVITY_STATE_UNRESTRICTED;
}
connman_info("%s: http return code %d, marking portal",
interface, http_status_code);
return CONNMAN_SERVICE_CONNECTIVITY_STATE_RESTRICTED;
}
connman_info("%s: http_get_failed, curl code %d, marking portal",
interface, code);
return CONNMAN_SERVICE_CONNECTIVITY_STATE_RESTRICTED;
}
/**
* Check the result of previous HTTP requests.
*/
static void check_request_status(void)
{
int remaining_updates;
CURLMsg* request_status;
struct portal_request *portal_request = NULL;
struct connman_service *service;
while ((request_status =
curl_multi_info_read(curl_multi_handle_, &remaining_updates))) {
if (CURLMSG_DONE != request_status->msg)
continue;
curl_easy_getinfo(request_status->easy_handle, CURLINFO_PRIVATE,
(char **)&portal_request);
service = portal_request->service;
if (service != NULL) {
enum connman_service_connectivity_state state =
check_connectivity_state(request_status,
portal_request->interface);
connman_service_set_connectivity_state(service, state);
} else {
_DBG_PORTAL("%s: Ignoring stale results",
portal_request->interface);
}
free_request(portal_request);
}
}
/**
* Check the timeout set by libcurl has expired.
*
* Returns TRUE if it has expired.
*/
static inline gboolean source_check_expiration(GSource* source, gint *timeout)
{
struct curl_source *src = (struct curl_source *)source;
struct timespec now;
gint delay_ms;
if (src->expiration.tv_sec == LONG_MAX) {
if (timeout)
*timeout = -1;
return FALSE;
}
clock_gettime(CLOCK_MONOTONIC, &now);
delay_ms = (src->expiration.tv_sec - now.tv_sec) * MSEC_PER_SEC
+ src->expiration.tv_nsec / (NSEC_PER_SEC / MSEC_PER_SEC)
- now.tv_nsec / (NSEC_PER_SEC / MSEC_PER_SEC);
if (delay_ms <= 0) {
_DBG_PORTAL("request has timeout'ed");
return TRUE;
} else {
if (timeout)
*timeout = delay_ms;
return FALSE;
}
}
/**
* Callback triggered before the main loop select operation.
*/
static gboolean source_prepare(GSource* source, gint* timeout)
{
return source_check_expiration(source, timeout);
}
/**
* Callback triggered when exiting the main loop select.
*/
static gboolean source_check(GSource* source)
{
int i, bmap;
struct curl_source *src = (struct curl_source *)source;
if (source_check_expiration(source, NULL))
return TRUE;
bmap = src->fd_bmap;
while ((i = __builtin_ffs(bmap) - 1) >= 0) {
if (src->poll_fds[i].revents)
return TRUE;
bmap &= ~(1 << i);
}
return FALSE;
}
/**
* Conversion table between glib poll events and libcurl select events.
*/
static struct {
int curl_event;
int poll_mask;
} event_flags[] = {
{ CURL_CSELECT_IN, G_IO_IN | G_IO_PRI },
{ CURL_CSELECT_OUT, G_IO_OUT },
{ CURL_CSELECT_ERR, G_IO_ERR | G_IO_HUP | G_IO_NVAL },
};
/**
* Process the request if there is an event of the file descriptors.
*/
static gboolean source_dispatch(GSource *source, GSourceFunc callback,
gpointer user_data)
{
int i, e, bmap;
int remaining = -1;
struct curl_source *src = (struct curl_source *)source;
CURLMcode mcode;
if (source_check_expiration(source, NULL)) {
mcode = curl_multi_perform(curl_multi_handle_, &remaining);
_DBG_PORTAL("mcode = %d, remaining = %d", mcode, remaining);
}
bmap = src->fd_bmap;
while ((i = __builtin_ffs(bmap) - 1) >= 0) {
if (src->poll_fds[i].revents) {
int ev_bitmask = 0;
for (e = 0; e < G_N_ELEMENTS(event_flags); e++)
if (src->poll_fds[i].revents &
event_flags[e].poll_mask)
ev_bitmask |= event_flags[e].curl_event;
mcode = curl_multi_socket_action(curl_multi_handle_,
src->poll_fds[i].fd,
ev_bitmask,
&remaining);
_DBG_PORTAL("mcode = %d, remaining = %d", mcode, remaining);
}
bmap &= ~(1 << i);
}
if (!remaining) {
/* no more pending transfer, set infinite timeout */
curl_source->expiration.tv_sec = LONG_MAX;
}
check_request_status();
return TRUE;
}
/**
* Define the source used to poll on the request descriptors.
*/
static GSourceFuncs source_callbacks = {
source_prepare, source_check, source_dispatch, 0, 0, 0
};
/**
* Update the file descriptors used to monitor in the main loop.
*/
static int socket_callback(CURL *easy, curl_socket_t s, int action,
void *userp, void *socketp)
{
gushort events = G_IO_ERR | G_IO_HUP;
struct curl_source *src = userp;
GPollFD *poll_fd = socketp;
int idx;
switch (action) {
case CURL_POLL_IN:
case CURL_POLL_OUT:
case CURL_POLL_INOUT:
if (socketp) {
g_source_remove_poll(&src->source, poll_fd);
} else {
if ((idx = (__builtin_ffs(~src->fd_bmap) - 1)) < 0) {
_DBG_PORTAL("Cannot set FD, no empty slot.\n");
break;
}
src->fd_bmap |= (1 << idx);
_DBG_PORTAL("FD#%d set (using slot %d)", s, idx);
poll_fd = &src->poll_fds[idx];
poll_fd->fd = s;
curl_multi_assign(curl_multi_handle_, s, poll_fd);
}
if ((action == CURL_POLL_IN) || (action == CURL_POLL_INOUT))
events |= G_IO_IN | G_IO_PRI;
if ((action == CURL_POLL_OUT) || (action == CURL_POLL_INOUT))
events |= G_IO_OUT;
poll_fd->events = events;
poll_fd->revents = 0;
g_source_add_poll(&src->source, poll_fd);
break;
case CURL_POLL_REMOVE:
if (poll_fd) {
poll_fd->revents = 0;
g_source_remove_poll(&src->source, poll_fd);
_DBG_PORTAL("FD#%d unset (using slot %d)",
poll_fd->fd,
(int) (poll_fd - src->poll_fds));
src->fd_bmap &= ~(1 << (poll_fd - src->poll_fds));
curl_multi_assign(curl_multi_handle_, s, NULL);
}
break;
}
return 0;
}
/**
* Compute and record the timeout requested by libcurl.
*/
static int timer_callback(CURLM *multi, long timeout_ms, void *userp)
{
struct curl_source *src = userp;
clock_gettime(CLOCK_MONOTONIC, &src->expiration);
if (timeout_ms >= 0) {
src->expiration.tv_sec +=
((long long)timeout_ms * 1000000LL) / NSEC_PER_SEC;
src->expiration.tv_nsec +=
((long long)timeout_ms * 1000000LL) % NSEC_PER_SEC;
if (src->expiration.tv_nsec >= NSEC_PER_SEC)
{
src->expiration.tv_sec++;
src->expiration.tv_nsec -= NSEC_PER_SEC;
}
} else {
src->expiration.tv_sec = LONG_MAX;
}
_DBG_PORTAL("set timeout %ld ms (expires %ld.%09ld)", timeout_ms,
src->expiration.tv_sec, src->expiration.tv_nsec);
return 0;
}
/**
* Returns TRUE if a service has entered a state where it may go away soon
*/
static gboolean should_cancel_request(struct connman_service *service)
{
const char *service_state;
if (service == NULL) {
_DBG_PORTAL("SKIP, NULL service");
return FALSE;
}
service_state = connman_service_get_state(service);
if ((g_strcmp0(service_state, "idle") == 0) ||
(g_strcmp0(service_state, "disconnect") == 0) ||
(g_strcmp0(service_state, "failure") == 0) ||
(g_strcmp0(service_state, "activation-failure") == 0))
{
_DBG_PORTAL("CANCEL, service %s state %s",
connman_service_get_identifier(service), service_state);
return TRUE;
}
return FALSE;
}
/**
* Returns TRUE if the new default route is "ready"
*/
static gboolean should_process_request(struct connman_service *service)
{
const char *service_state;
if (service == NULL) {
_DBG_PORTAL("SKIP, NULL service");
return FALSE;
}
service_state = connman_service_get_state(service);
if (g_strcmp0(service_state, "ready") != 0) {
_DBG_PORTAL("SKIP, service %s state %s != ready",
connman_service_get_identifier(service), service_state);
return FALSE;
}
return TRUE;
}
/**
* Unref a service on the idle thread.
*/
static gboolean __unref_service(gpointer data)
{
struct connman_service *service = data;
const char *interface = connman_service_get_interface(service);
_DBG_PORTAL("%s: unref service",
interface != NULL ? interface : "unknown");
connman_service_unref(service);
return FALSE;
}
/**
* Cycle a service to the ONLINE state. Used when portal check
* is disabled for the service (called from the idle loop).
*/
static gboolean __mark_unrestricted(gpointer data)
{
struct connman_service *service = data;
const char *interface = connman_service_get_interface(service);
_DBG_PORTAL("%s: BYPASS, mark service ONLINE",
interface != NULL ? interface : "unknown");
connman_service_set_connectivity_state(service,
CONNMAN_SERVICE_CONNECTIVITY_STATE_UNRESTRICTED);
connman_service_unref(service);
return FALSE;
}
static int debug_function(CURL *handle, curl_infotype type,
char *data, size_t size, void *userptr)
{
if (type == CURLINFO_TEXT)
_DBG_PORTAL("curl: %s", data);
return 0;
}
static void __portal_test(struct connman_service *service)
{
struct connman_device *device;
const char *interface;
const struct connman_resolver_state *resolver_state;
CURLcode ecode;
CURLMcode mcode;
CURL * request_handle;
struct portal_request *portal_request;
connman_bool_t first = TRUE;
GString *servers = NULL;
char **p;
static const struct {
CURLoption option;
unsigned int parameter;
const char *option_name;
} options[] = {
/* Do not let curl cache DNS entries */
{CURLOPT_DNS_CACHE_TIMEOUT, 0, "DNS_CACHE_TIMEOUT"},
/* Do not allow curl to reuse the connection */
{CURLOPT_FORBID_REUSE, 1, "FORBID_REUSE"},
/* Set a timeout for the request in seconds */
{CURLOPT_TIMEOUT, REQUEST_TIMEOUT_S, "TIMEOUT"},
/* Set a timeout for the TCP/IP connect in seconds */
{CURLOPT_CONNECTTIMEOUT, REQUEST_TIMEOUT_S, "CONNECTTIMEOUT"},
/* Ensure CURL does not send signals or install handlers */
{CURLOPT_NOSIGNAL, 1, "NOSIGNAL"},
/* Instruct curl to generate debugging information */
{CURLOPT_VERBOSE, 1, "VERBOSE"},
};
int i;
if (__connman_service_check_portal(service) == FALSE) {
/*
* The service or device is not meant to do a portal
* check; mark it online immediately.
*/
g_idle_add(__mark_unrestricted, connman_service_ref(service));
return;
}
device = connman_service_get_device(service);
if (device == NULL) {
connman_error("%s: no device for service %p",
__func__, service);
return;
}
interface = connman_device_get_interface(device);
if (interface == NULL) {
connman_error("%s: no interface for device %p (service %p)",
__func__, device, service);
return;
}
resolver_state = connman_resolver_lookup(interface);
if (resolver_state == NULL) {
connman_error("%s: no resolver state for service %p",
__func__, service);
return;
}
_DBG_PORTAL("%s: Checking %s on %s",
interface,
__connman_profile_get_portal_url(),
connman_service_get_identifier(service));
request_handle = curl_easy_init();
if (request_handle == NULL) {
connman_error("%s: easy request is NULL", __func__);
return;
}
/* The portal request will take ownership of the request_handle */
portal_request = new_request(service, device, interface, request_handle);
if (portal_request == NULL) {
connman_error("%s: Cannot allocate portal request.", __func__);
curl_easy_cleanup(request_handle);
return;
}
/* Set standard options */
for (i = 0; i < lengthof(options); i++) {
ecode = curl_easy_setopt(request_handle,
options[i].option,
options[i].parameter);
if (ecode != CURLE_OK) {
connman_error("%s :%s = %s", __func__,
options[i].option_name,
curl_easy_strerror(ecode));
goto error;
}
}
/* Set pointer options */
ecode = curl_easy_setopt(request_handle,
CURLOPT_PRIVATE, portal_request);
if (ecode != CURLE_OK) {
connman_error("%s: Unable to set PRIVATE: %s", __func__,
curl_easy_strerror(ecode));
goto error;
}
ecode = curl_easy_setopt(request_handle, CURLOPT_URL,
__connman_profile_get_portal_url());
if (ecode != CURLE_OK) {
connman_error("%s: Unable to set URL: %s", __func__,
curl_easy_strerror(ecode));
goto error;
}
ecode = curl_easy_setopt(request_handle, CURLOPT_INTERFACE, interface);
if (ecode != CURLE_OK) {
connman_error("%s: Unable to set INTERFACE: %s", __func__,
curl_easy_strerror(ecode));
goto error;
}
/* name servers */
servers = g_string_sized_new(128);
for(p = resolver_state->servers; *p != NULL; p++) {
if (!first)
g_string_append_c(servers, ',');
g_string_append(servers, *p);
first = FALSE;
}
_DBG_PORTAL("%s: Using name servers %s", interface, servers->str);
ecode = curl_easy_setopt(request_handle,
CURLOPT_DNS_SERVERS, servers->str);
if (ecode != CURLE_OK) {
connman_error("%s: Unable to set DNS_SERVERS: %s",
__func__, curl_easy_strerror(ecode));
goto error;
}
ecode = curl_easy_setopt(request_handle,
CURLOPT_DEBUGFUNCTION, debug_function);
if (ecode != CURLE_OK) {
connman_error("%s: Unable to set DEBUGFUNCTION: %s",
__func__, curl_easy_strerror(ecode));
goto error;
}
mcode = curl_multi_add_handle(curl_multi_handle_, request_handle);
if (mcode != CURLM_OK) {
connman_error("%s: curl_multi_add_handle() = %s", __func__,
curl_multi_strerror(mcode));
goto error;
}
portal_request->handle_added = TRUE;
cancel_old_requests(portal_request);
curl_multi_socket_action(curl_multi_handle_, CURL_SOCKET_BAD, 0, &i);
g_string_free(servers, TRUE);
return;
error:
g_string_free(servers, TRUE);
free_request(portal_request);
}
/**
* Check to see if we're in an IP restrict pool everytime a service
* becomes ready.
*/
static void portal_service_state_changed(struct connman_service *service)
{
if (should_cancel_request(service)) {
/*
* cancel_service_requests might unref the last
* reference to the service. That would free the
* service object and mean that callers higher up the
* stack might dereference an invalid service object.
* Prevent that by adding a reference here, and then
* releasing that reference on the idle thread.
*/
connman_service_ref(service);
cancel_service_requests(service);
g_idle_add(__unref_service, service);
return;
}
if (!should_process_request(service))
return;
__portal_test(service);
}
/**
* recheck a service to see if it is in the portal state or online
*/
void __connman_portal_service_recheck_state(struct connman_service *service)
{
if (service_has_request(service))
return;
__portal_test(service);
}
static struct connman_notifier portal_notifier = {
.name = "portal",
.priority = CONNMAN_NOTIFIER_PRIORITY_LOW,
.service_state_changed = portal_service_state_changed,
};
int __connman_portal_init(void)
{
CURLMcode mcode;
if (connman_notifier_register(&portal_notifier) < 0) {
connman_error("Failed to register the portal notifier");
return -1;
}
curl_multi_handle_ = curl_multi_init();
if (!curl_multi_handle_) {
_DBG_PORTAL("Unable to initialize libcurl");
connman_notifier_unregister(&portal_notifier);
return -1;
}
curl_source = (struct curl_source *)
g_source_new(&source_callbacks, sizeof(struct curl_source));
curl_source->fd_bmap = 0;
curl_source->expiration.tv_sec = LONG_MAX;
mcode = curl_multi_setopt(curl_multi_handle_, CURLMOPT_SOCKETFUNCTION,
socket_callback);
if (mcode != CURLM_OK) {
connman_error("%s: curl_multi_setopt(SOCKETFUNCTION) = %s",
__func__, curl_multi_strerror(mcode));
goto err_source;
}
mcode = curl_multi_setopt(curl_multi_handle_, CURLMOPT_SOCKETDATA,
curl_source);
if (mcode != CURLM_OK) {
connman_error("%s: curl_multi_setopt(SOCKETDATA) = %s",
__func__, curl_multi_strerror(mcode));
goto err_source;
}
mcode = curl_multi_setopt(curl_multi_handle_, CURLMOPT_TIMERFUNCTION,
timer_callback);
if (mcode != CURLM_OK) {
connman_error("%s: curl_multi_setopt(TIMERFUNCTION) = %s",
__func__, curl_multi_strerror(mcode));
goto err_source;
}
mcode = curl_multi_setopt(curl_multi_handle_, CURLMOPT_TIMERDATA,
curl_source);
if (mcode != CURLM_OK) {
connman_error("%s: curl_multi_setopt(TIMERDATA) = %s",
__func__, curl_multi_strerror(mcode));
goto err_source;
}
g_source_attach(&curl_source->source, NULL);
return 0;
err_source:
g_source_unref(&curl_source->source);
curl_multi_cleanup(curl_multi_handle_);
connman_notifier_unregister(&portal_notifier);
return -1;
}
void __connman_portal_cleanup(void)
{
connman_notifier_unregister(&portal_notifier);
g_source_destroy(&curl_source->source);
g_source_unref(&curl_source->source);
curl_multi_cleanup(curl_multi_handle_);
}