/* px-manager.c * * Copyright 2022-2023 The Libproxy Team * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "px-backend-config.h" #include #include #include #include "px-manager.h" #include "px-plugin-config.h" #include "px-plugin-pacrunner.h" #ifdef HAVE_CONFIG_ENV #include #endif #ifdef HAVE_CONFIG_GNOME #include #endif #ifdef HAVE_CONFIG_KDE #include #endif #ifdef HAVE_CONFIG_OSX #include #endif #ifdef HAVE_CONFIG_SYSCONFIG #include #endif #ifdef HAVE_CONFIG_WINDOWS #include #endif #ifdef HAVE_PACRUNNER_DUKTAPE #include #endif #ifdef HAVE_CURL #include #endif enum { PROP_0, PROP_CONFIG_PLUGIN, PROP_CONFIG_OPTION, LAST_PROP }; static GParamSpec *obj_properties[LAST_PROP]; /** * PxManager: * * Manage libproxy modules */ struct _PxManager { GObject parent_instance; GList *config_plugins; GList *pacrunner_plugins; GNetworkMonitor *network_monitor; #ifdef HAVE_CURL CURL *curl; #endif char *config_plugin; char *config_option; gboolean online; gboolean wpad; GBytes *pac_data; char *pac_url; }; G_DEFINE_TYPE (PxManager, px_manager, G_TYPE_OBJECT) G_DEFINE_QUARK (px - manager - error - quark, px_manager_error) static void px_manager_on_network_changed (GNetworkMonitor *monitor, gboolean network_available, gpointer user_data) { PxManager *self = PX_MANAGER (user_data); g_debug ("%s: Network connection changed, clearing pac data", __FUNCTION__); self->wpad = FALSE; self->online = network_available; g_clear_pointer (&self->pac_url, g_free); g_clear_pointer (&self->pac_data, g_bytes_unref); } static gint config_order_compare (gconstpointer a, gconstpointer b) { PxConfig *config_a = (PxConfig *)a; PxConfig *config_b = (PxConfig *)b; PxConfigInterface *ifc_a = PX_CONFIG_GET_IFACE (config_a); PxConfigInterface *ifc_b = PX_CONFIG_GET_IFACE (config_b); if (ifc_a->priority < ifc_b->priority) return -1; if (ifc_a->priority == ifc_b->priority) return 0; return 1; } static void px_manager_add_config_plugin (PxManager *self, GType type) { PxConfig *config = g_object_new (type, "config-option", self->config_option, NULL); PxConfigInterface *ifc = PX_CONFIG_GET_IFACE (config); const char *env = g_getenv ("PX_FORCE_CONFIG"); const char *force_config = self->config_plugin ? self->config_plugin : env; if (!force_config || g_strcmp0 (ifc->name, force_config) == 0) self->config_plugins = g_list_insert_sorted (self->config_plugins, config, config_order_compare); } static void px_manager_add_pacrunner_plugin (PxManager *self, GType type) { PxPacRunner *pacrunner = g_object_new (type, NULL); self->pacrunner_plugins = g_list_append (self->pacrunner_plugins, pacrunner); } static void px_manager_constructed (GObject *object) { PxManager *self = PX_MANAGER (object); if (g_getenv ("PX_DEBUG")) { const gchar *g_messages_debug; g_messages_debug = g_getenv ("G_MESSAGES_DEBUG"); if (!g_messages_debug) { g_setenv ("G_MESSAGES_DEBUG", G_LOG_DOMAIN, TRUE); } else { g_autofree char *new_g_messages_debug = NULL; new_g_messages_debug = g_strconcat (g_messages_debug, " ", G_LOG_DOMAIN, NULL); if (new_g_messages_debug) g_setenv ("G_MESSAGES_DEBUG", new_g_messages_debug, TRUE); } } #ifdef HAVE_CONFIG_ENV px_manager_add_config_plugin (self, PX_CONFIG_TYPE_ENV); #endif #ifdef HAVE_CONFIG_GNOME px_manager_add_config_plugin (self, PX_CONFIG_TYPE_GNOME); #endif #ifdef HAVE_CONFIG_KDE px_manager_add_config_plugin (self, PX_CONFIG_TYPE_KDE); #endif #ifdef HAVE_CONFIG_OSX px_manager_add_config_plugin (self, PX_CONFIG_TYPE_OSX); #endif #ifdef HAVE_CONFIG_SYSCONFIG px_manager_add_config_plugin (self, PX_CONFIG_TYPE_SYSCONFIG); #endif #ifdef HAVE_CONFIG_WINDOWS px_manager_add_config_plugin (self, PX_CONFIG_TYPE_WINDOWS); #endif g_debug ("Active config plugins:\n"); for (GList *list = self->config_plugins; list && list->data; list = list->next) { PxConfig *config = list->data; PxConfigInterface *ifc = PX_CONFIG_GET_IFACE (config); g_debug (" - %s\n", ifc->name); } #ifdef HAVE_PACRUNNER_DUKTAPE px_manager_add_pacrunner_plugin (self, PX_PACRUNNER_TYPE_DUKTAPE); #endif self->pac_data = NULL; self->network_monitor = g_network_monitor_get_default (); g_signal_connect_object (G_OBJECT (self->network_monitor), "network-changed", G_CALLBACK (px_manager_on_network_changed), self, 0); px_manager_on_network_changed (self->network_monitor, g_network_monitor_get_network_available (self->network_monitor), self); g_debug ("%s: Up and running", __FUNCTION__); } static void px_manager_dispose (GObject *object) { PxManager *self = PX_MANAGER (object); for (GList *list = self->config_plugins; list && list->data; list = list->next) g_clear_object (&list->data); for (GList *list = self->pacrunner_plugins; list && list->data; list = list->next) g_clear_object (&list->data); g_clear_pointer (&self->config_plugin, g_free); G_OBJECT_CLASS (px_manager_parent_class)->dispose (object); } static void px_manager_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { PxManager *self = PX_MANAGER (object); switch (prop_id) { case PROP_CONFIG_PLUGIN: self->config_plugin = g_strdup (g_value_get_string (value)); break; case PROP_CONFIG_OPTION: self->config_option = g_strdup (g_value_get_string (value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); } } static void px_manager_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { switch (prop_id) { case PROP_CONFIG_PLUGIN: break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void px_manager_class_init (PxManagerClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->constructed = px_manager_constructed; object_class->dispose = px_manager_dispose; object_class->set_property = px_manager_set_property; object_class->get_property = px_manager_get_property; obj_properties[PROP_CONFIG_PLUGIN] = g_param_spec_string ("config-plugin", NULL, NULL, NULL, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); obj_properties[PROP_CONFIG_OPTION] = g_param_spec_string ("config-option", NULL, NULL, NULL, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); g_object_class_install_properties (object_class, LAST_PROP, obj_properties); } static void px_manager_init (PxManager *self) { } /** * px_manager_new_with_options: * @optname1: name of first property to set * @...: value of @optname1, followed by additional property/value pairs * * Create a new `PxManager` with the specified options. * * Returns: the newly created `PxManager` */ PxManager * px_manager_new_with_options (const char *optname1, ...) { PxManager *self; va_list ap; va_start (ap, optname1); self = (PxManager *)g_object_new_valist (PX_TYPE_MANAGER, optname1, ap); va_end (ap); return self; } /** * px_manager_new: * * Create a new `PxManager`. * * Returns: the newly created `PxManager` */ PxManager * px_manager_new (void) { return px_manager_new_with_options (NULL); } #ifdef HAVE_CURL static size_t store_data (void *contents, size_t size, size_t nmemb, void *user_pointer) { GByteArray *byte_array = user_pointer; size_t real_size = size * nmemb; g_byte_array_append (byte_array, contents, real_size); return real_size; } #endif /** * px_manager_pac_download: * @self: a px manager * @uri: PAC uri * * Downloads a PAC file from provided @url. * * Returns: (nullable): a newly created `GBytes` containing PAC data, or %NULL on error. */ GBytes * px_manager_pac_download (PxManager *self, const char *uri) { #ifdef HAVE_CURL GByteArray *byte_array = g_byte_array_new (); CURLcode res; const char *url = uri; if (!self->curl) self->curl = curl_easy_init (); if (!self->curl) return NULL; if (g_str_has_prefix (url, "pac+")) url += 4; curl_easy_setopt (self->curl, CURLOPT_NOSIGNAL, 1); curl_easy_setopt (self->curl, CURLOPT_FOLLOWLOCATION, 1); curl_easy_setopt (self->curl, CURLOPT_NOPROXY, "*"); curl_easy_setopt (self->curl, CURLOPT_CONNECTTIMEOUT, 30); curl_easy_setopt (self->curl, CURLOPT_USERAGENT, "libproxy"); curl_easy_setopt (self->curl, CURLOPT_URL, url); curl_easy_setopt (self->curl, CURLOPT_WRITEFUNCTION, store_data); curl_easy_setopt (self->curl, CURLOPT_WRITEDATA, byte_array); res = curl_easy_perform (self->curl); if (res != CURLE_OK) { g_debug ("%s: Could not download data: %s", __FUNCTION__, curl_easy_strerror (res)); return NULL; } return g_byte_array_free_to_bytes (byte_array); #else return NULL; #endif } /** * px_manager_get_configuration: * @self: a px manager * @uri: PAC uri * @error: a #GError * * Get raw proxy configuration for gien @uri. * * Returns: (transfer full) (nullable): a newly created `GStrv` containing configuration data for @uri. */ char ** px_manager_get_configuration (PxManager *self, GUri *uri, GError **error) { g_autoptr (GStrvBuilder) builder = g_strv_builder_new (); for (GList *list = self->config_plugins; list && list->data; list = list->next) { PxConfig *config = PX_CONFIG (list->data); PxConfigInterface *ifc = PX_CONFIG_GET_IFACE (config); ifc->get_config (config, uri, builder); } return g_strv_builder_end (builder); } static void px_manager_run_pac (PxPacRunner *pacrunner, GBytes *pac, GUri *uri, GStrvBuilder *builder) { PxPacRunnerInterface *ifc = PX_PAC_RUNNER_GET_IFACE (pacrunner); g_auto (GStrv) proxies_split = NULL; char *pac_response; if (!ifc->set_pac (PX_PAC_RUNNER (pacrunner), pac)) return; pac_response = ifc->run (PX_PAC_RUNNER (pacrunner), uri); /* Split line to handle multiple proxies */ proxies_split = g_strsplit (pac_response, ";", -1); for (int idx = 0; idx < g_strv_length (proxies_split); idx++) { char *line = g_strstrip (proxies_split[idx]); g_auto (GStrv) word_split = g_strsplit (line, " ", -1); g_autoptr (GUri) proxy_uri = NULL; char *method; char *server; /* Check for syntax "METHOD SERVER" */ if (g_strv_length (word_split) == 2) { g_autofree char *uri_string = NULL; g_autofree char *proxy_string = NULL; method = word_split[0]; server = word_split[1]; uri_string = g_strconcat ("http://", server, NULL); proxy_uri = g_uri_parse (uri_string, G_URI_FLAGS_PARSE_RELAXED, NULL); if (!proxy_uri) continue; if (g_ascii_strncasecmp (method, "proxy", 5) == 0) { proxy_string = g_uri_to_string (proxy_uri); } else if (g_ascii_strncasecmp (method, "socks4a", 7) == 0) { proxy_string = g_strconcat ("socks4a://", server, NULL); } else if (g_ascii_strncasecmp (method, "socks4", 6) == 0) { proxy_string = g_strconcat ("socks4://", server, NULL); } else if (g_ascii_strncasecmp (method, "socks5", 6) == 0) { proxy_string = g_strconcat ("socks5://", server, NULL); } else if (g_ascii_strncasecmp (method, "socks", 5) == 0) { proxy_string = g_strconcat ("socks://", server, NULL); } px_strv_builder_add_proxy (builder, proxy_string); } else { /* Syntax not found, returning direct */ px_strv_builder_add_proxy (builder, "direct://"); } } } static gboolean px_manager_expand_wpad (PxManager *self, GUri *uri) { const char *scheme = g_uri_get_scheme (uri); gboolean ret = FALSE; if (g_strcmp0 (scheme, "wpad") == 0) { ret = TRUE; if (!self->wpad) { g_clear_pointer (&self->pac_data, g_bytes_unref); g_clear_pointer (&self->pac_url, g_free); self->wpad = TRUE; } if (!self->pac_data) { GUri *wpad_url = g_uri_parse ("http://wpad/wpad.dat", G_URI_FLAGS_PARSE_RELAXED, NULL); g_debug ("%s: Trying to find the PAC using WPAD...", __FUNCTION__); self->pac_url = g_uri_to_string (wpad_url); self->pac_data = px_manager_pac_download (self, self->pac_url); if (!self->pac_data) { g_clear_pointer (&self->pac_url, g_free); ret = FALSE; } } } return ret; } static gboolean px_manager_expand_pac (PxManager *self, GUri *uri) { gboolean ret = FALSE; const char *scheme = g_uri_get_scheme (uri); if (g_str_has_prefix (scheme, "pac+")) { ret = TRUE; if (self->wpad) self->wpad = FALSE; if (self->pac_data) { g_autofree char *uri_str = g_uri_to_string (uri); if (g_strcmp0 (self->pac_url, uri_str) != 0) { g_clear_pointer (&self->pac_url, g_free); g_clear_pointer (&self->pac_data, g_bytes_unref); } } if (!self->pac_data) { self->pac_url = g_uri_to_string (uri); self->pac_data = px_manager_pac_download (self, self->pac_url); if (!self->pac_data) { g_warning ("%s: Unable to download PAC from %s while online = %d!", __FUNCTION__, self->pac_url, self->online); g_clear_pointer (&self->pac_url, g_free); ret = FALSE; } else { g_debug ("%s: PAC recevied!", __FUNCTION__); } } } return ret; } /** * px_manager_get_proxies_sync: * @self: a px manager * @url: a url * * Get proxies for giben @url. * * Returns: (transfer full) (nullable): a newly created `GStrv` containing proxy related information. */ char ** px_manager_get_proxies_sync (PxManager *self, const char *url, GError **error) { g_autoptr (GStrvBuilder) builder = g_strv_builder_new (); g_autoptr (GUri) uri = g_uri_parse (url, G_URI_FLAGS_PARSE_RELAXED, error); g_auto (GStrv) config = NULL; g_debug ("%s: url=%s online=%d", __FUNCTION__, url ? url : "?", self->online); if (!uri || !self->online) { px_strv_builder_add_proxy (builder, "direct://"); return g_strv_builder_end (builder); } config = px_manager_get_configuration (self, uri, error); for (int idx = 0; idx < g_strv_length (config); idx++) { GUri *conf_url = g_uri_parse (config[idx], G_URI_FLAGS_PARSE_RELAXED, NULL); g_debug ("%s: Config[%d] = %s", __FUNCTION__, idx, config[idx]); if (px_manager_expand_wpad (self, conf_url) || px_manager_expand_pac (self, conf_url)) { GList *list; for (list = self->pacrunner_plugins; list && list->data; list = list->next) { PxPacRunner *pacrunner = PX_PAC_RUNNER (list->data); px_manager_run_pac (pacrunner, self->pac_data, uri, builder); } } else if (!g_str_has_prefix (g_uri_get_scheme (conf_url), "wpad") && !g_str_has_prefix (g_uri_get_scheme (conf_url), "pac+")) { px_strv_builder_add_proxy (builder, g_uri_to_string (conf_url)); } } /* In case no proxy could be found, assume direct connection */ if (((GPtrArray *)builder)->len == 0) px_strv_builder_add_proxy (builder, "direct://"); for (int idx = 0; idx < ((GPtrArray *)builder)->len; idx++) g_debug ("%s: Proxy[%d] = %s", __FUNCTION__, idx, (char *)((GPtrArray *)builder)->pdata[idx]); return g_strv_builder_end (builder); } void px_strv_builder_add_proxy (GStrvBuilder *builder, const char *value) { for (int idx = 0; idx < ((GPtrArray *)builder)->len; idx++) { if (g_strcmp0 ((char *)((GPtrArray *)builder)->pdata[idx], value) == 0) return; } g_strv_builder_add (builder, value); } static gboolean ignore_domain (GUri *uri, char *ignore) { g_auto (GStrv) ignore_split = g_strsplit (ignore, ":", -1); const char *host = g_uri_get_host (uri); char *ig_host; int ig_port = -1; int port = g_uri_get_port (uri); /* Get our ignore pattern's hostname and port */ ig_host = ignore_split[0]; if (g_strv_length (ignore_split) == 2) ig_port = atoi (ignore_split[1]); /* Hostname match (domain.com or domain.com:80) */ if (g_strcmp0 (host, ig_host) == 0) return (ig_port == -1 || port == ig_port); /* Endswith (.domain.com or .domain.com:80) */ if (ig_host[0] == '.' && g_str_has_suffix (host, ig_host)) return (ig_port == -1 || port == ig_port); /* Glob (*.domain.com or *.domain.com:80) */ if (ig_host[0] == '*' && g_str_has_suffix (host, ig_host + 1)) return (ig_port == -1 || port == ig_port); /* No match was found */ return FALSE; } static gboolean ignore_hostname (GUri *uri, char *ignore) { const char *host = g_uri_get_host (uri); if (g_strcmp0 (ignore, "") == 0 && strchr (host, ':') == NULL && strchr (host, '.') == NULL) return TRUE; return FALSE; } static gboolean ignore_ip (GUri *uri, char *ignore) { GInetAddress *inet_address1; GInetAddress *inet_address2; g_auto (GStrv) ignore_split = NULL; const char *uri_host = g_uri_get_host (uri); gboolean is_ip1 = FALSE; gboolean is_ip2 = g_hostname_is_ip_address (ignore); int port = g_uri_get_port (uri); int ig_port = -1; gboolean result; if (uri_host) is_ip1 = g_hostname_is_ip_address (uri_host); /* * IPv4 * IPv6 */ if (!is_ip1 || !is_ip2) return FALSE; /* * IPv4/CIDR * IPv4/IPv4 * IPv6/CIDR * IPv6/IPv6 */ /* MISSING */ /* * IPv4:port * [IPv6]:port */ ignore_split = g_strsplit (ignore, ":", -1); if (g_strv_length (ignore_split) == 2) ig_port = atoi (ignore_split[1]); inet_address1 = g_inet_address_new_from_string (g_uri_get_host (uri)); inet_address2 = g_inet_address_new_from_string (ignore); result = g_inet_address_equal (inet_address1, inet_address2); return port != -1 ? ((port == ig_port) && result) : result; } gboolean px_manager_is_ignore (GUri *uri, GStrv ignores) { if (!ignores) return FALSE; for (int idx = 0; idx < g_strv_length (ignores); idx++) { if (ignore_hostname (uri, ignores[idx])) return TRUE; if (ignore_domain (uri, ignores[idx])) return TRUE; if (ignore_ip (uri, ignores[idx])) return TRUE; } return FALSE; }