diff options
| author | Carlos Garcia Campos <cgarcia@igalia.com> | 2020-10-07 14:03:17 +0200 |
|---|---|---|
| committer | Carlos Garcia Campos <cgarcia@igalia.com> | 2020-10-09 11:34:08 +0200 |
| commit | 869e580033ec92cc8782f941da04d623b481e3e4 (patch) | |
| tree | 657cb4fdd4d7cb055a78d43593ffeab1b427da67 /libsoup/server | |
| parent | 6c2ef5ae4fad12caddb1ff4f07bb2f4d2af9386e (diff) | |
| download | libsoup-869e580033ec92cc8782f941da04d623b481e3e4.tar.gz | |
server: move SoupServer code to server directory
Diffstat (limited to 'libsoup/server')
| -rw-r--r-- | libsoup/server/soup-auth-domain-basic.c | 348 | ||||
| -rw-r--r-- | libsoup/server/soup-auth-domain-basic.h | 35 | ||||
| -rw-r--r-- | libsoup/server/soup-auth-domain-digest.c | 454 | ||||
| -rw-r--r-- | libsoup/server/soup-auth-domain-digest.h | 39 | ||||
| -rw-r--r-- | libsoup/server/soup-auth-domain.c | 635 | ||||
| -rw-r--r-- | libsoup/server/soup-auth-domain.h | 92 | ||||
| -rw-r--r-- | libsoup/server/soup-message-server-io.c | 304 | ||||
| -rw-r--r-- | libsoup/server/soup-path-map.c | 192 | ||||
| -rw-r--r-- | libsoup/server/soup-path-map.h | 26 | ||||
| -rw-r--r-- | libsoup/server/soup-server-private.h | 23 | ||||
| -rw-r--r-- | libsoup/server/soup-server.c | 2364 | ||||
| -rw-r--r-- | libsoup/server/soup-server.h | 180 |
12 files changed, 4692 insertions, 0 deletions
diff --git a/libsoup/server/soup-auth-domain-basic.c b/libsoup/server/soup-auth-domain-basic.c new file mode 100644 index 00000000..9a850050 --- /dev/null +++ b/libsoup/server/soup-auth-domain-basic.c @@ -0,0 +1,348 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * soup-auth-domain-basic.c: HTTP Basic Authentication (server-side) + * + * Copyright (C) 2007 Novell, Inc. + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <string.h> + +#include "soup-auth-domain-basic.h" +#include "soup.h" + +/** + * SECTION:soup-auth-domain-basic + * @short_description: Server-side "Basic" authentication + * + * #SoupAuthDomainBasic handles the server side of HTTP "Basic" (ie, + * cleartext password) authentication. + **/ + +enum { + PROP_0, + + PROP_AUTH_CALLBACK, + PROP_AUTH_DATA, + + LAST_PROP +}; + +struct _SoupAuthDomainBasic { + SoupAuthDomain parent; +}; + +typedef struct { + SoupAuthDomainBasicAuthCallback auth_callback; + gpointer auth_data; + GDestroyNotify auth_dnotify; +} SoupAuthDomainBasicPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (SoupAuthDomainBasic, soup_auth_domain_basic, SOUP_TYPE_AUTH_DOMAIN) + +static void +soup_auth_domain_basic_init (SoupAuthDomainBasic *basic) +{ +} + +static void +soup_auth_domain_basic_finalize (GObject *object) +{ + SoupAuthDomainBasicPrivate *priv = + soup_auth_domain_basic_get_instance_private (SOUP_AUTH_DOMAIN_BASIC (object)); + + if (priv->auth_dnotify) + priv->auth_dnotify (priv->auth_data); + + G_OBJECT_CLASS (soup_auth_domain_basic_parent_class)->finalize (object); +} + +static void +soup_auth_domain_basic_set_property (GObject *object, guint prop_id, + const GValue *value, GParamSpec *pspec) +{ + SoupAuthDomainBasicPrivate *priv = + soup_auth_domain_basic_get_instance_private (SOUP_AUTH_DOMAIN_BASIC (object)); + + switch (prop_id) { + case PROP_AUTH_CALLBACK: + priv->auth_callback = g_value_get_pointer (value); + break; + case PROP_AUTH_DATA: + if (priv->auth_dnotify) { + priv->auth_dnotify (priv->auth_data); + priv->auth_dnotify = NULL; + } + priv->auth_data = g_value_get_pointer (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +soup_auth_domain_basic_get_property (GObject *object, guint prop_id, + GValue *value, GParamSpec *pspec) +{ + SoupAuthDomainBasicPrivate *priv = + soup_auth_domain_basic_get_instance_private (SOUP_AUTH_DOMAIN_BASIC (object)); + + switch (prop_id) { + case PROP_AUTH_CALLBACK: + g_value_set_pointer (value, priv->auth_callback); + break; + case PROP_AUTH_DATA: + g_value_set_pointer (value, priv->auth_data); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +/** + * soup_auth_domain_basic_new: + * @optname1: name of first option, or %NULL + * @...: option name/value pairs + * + * Creates a #SoupAuthDomainBasic. You must set the + * %SOUP_AUTH_DOMAIN_REALM parameter, to indicate the realm name to be + * returned with the authentication challenge to the client. Other + * parameters are optional. + * + * Return value: the new #SoupAuthDomain + **/ +SoupAuthDomain * +soup_auth_domain_basic_new (const char *optname1, ...) +{ + SoupAuthDomain *domain; + va_list ap; + + va_start (ap, optname1); + domain = (SoupAuthDomain *)g_object_new_valist (SOUP_TYPE_AUTH_DOMAIN_BASIC, + optname1, ap); + va_end (ap); + + g_return_val_if_fail (soup_auth_domain_get_realm (domain) != NULL, NULL); + + return domain; +} + +/** + * SoupAuthDomainBasicAuthCallback: + * @domain: (type SoupAuthDomainBasic): the domain + * @msg: the message being authenticated + * @username: the username provided by the client + * @password: the password provided by the client + * @user_data: the data passed to soup_auth_domain_basic_set_auth_callback() + * + * Callback used by #SoupAuthDomainBasic for authentication purposes. + * The application should verify that @username and @password and valid + * and return %TRUE or %FALSE. + * + * If you are maintaining your own password database (rather than + * using the password to authenticate against some other system like + * PAM or a remote server), you should make sure you know what you are + * doing. In particular, don't store cleartext passwords, or + * easily-computed hashes of cleartext passwords, even if you don't + * care that much about the security of your server, because users + * will frequently use the same password for multiple sites, and so + * compromising any site with a cleartext (or easily-cracked) password + * database may give attackers access to other more-interesting sites + * as well. + * + * Return value: %TRUE if @username and @password are valid + **/ + +/** + * soup_auth_domain_basic_set_auth_callback: + * @domain: (type SoupAuthDomainBasic): the domain + * @callback: the callback + * @user_data: data to pass to @auth_callback + * @dnotify: destroy notifier to free @user_data when @domain + * is destroyed + * + * Sets the callback that @domain will use to authenticate incoming + * requests. For each request containing authorization, @domain will + * invoke the callback, and then either accept or reject the request + * based on @callback's return value. + * + * You can also set the auth callback by setting the + * %SOUP_AUTH_DOMAIN_BASIC_AUTH_CALLBACK and + * %SOUP_AUTH_DOMAIN_BASIC_AUTH_DATA properties, which can also be + * used to set the callback at construct time. + **/ +void +soup_auth_domain_basic_set_auth_callback (SoupAuthDomain *domain, + SoupAuthDomainBasicAuthCallback callback, + gpointer user_data, + GDestroyNotify dnotify) +{ + SoupAuthDomainBasicPrivate *priv = + soup_auth_domain_basic_get_instance_private (SOUP_AUTH_DOMAIN_BASIC (domain)); + + if (priv->auth_dnotify) + priv->auth_dnotify (priv->auth_data); + + priv->auth_callback = callback; + priv->auth_data = user_data; + priv->auth_dnotify = dnotify; + + g_object_notify (G_OBJECT (domain), SOUP_AUTH_DOMAIN_BASIC_AUTH_CALLBACK); + g_object_notify (G_OBJECT (domain), SOUP_AUTH_DOMAIN_BASIC_AUTH_DATA); +} + +static void +pw_free (char *pw) +{ + memset (pw, 0, strlen (pw)); + g_free (pw); +} + +static gboolean +parse_basic (SoupMessage *msg, const char *header, + char **username, char **password) +{ + char *decoded, *colon; + gsize len, plen; + + if (!header || (strncmp (header, "Basic ", 6) != 0)) + return FALSE; + + decoded = (char *)g_base64_decode (header + 6, &len); + if (!decoded) + return FALSE; + + colon = memchr (decoded, ':', len); + if (!colon) { + pw_free (decoded); + return FALSE; + } + *colon = '\0'; + plen = len - (colon - decoded) - 1; + + *password = g_strndup (colon + 1, plen); + memset (colon + 1, 0, plen); + *username = decoded; + return TRUE; +} + +static char * +soup_auth_domain_basic_accepts (SoupAuthDomain *domain, SoupMessage *msg, + const char *header) +{ + SoupAuthDomainBasicPrivate *priv = + soup_auth_domain_basic_get_instance_private (SOUP_AUTH_DOMAIN_BASIC (domain)); + char *username, *password; + gboolean ok = FALSE; + + if (!parse_basic (msg, header, &username, &password)) + return NULL; + + if (priv->auth_callback) { + ok = priv->auth_callback (domain, msg, username, password, + priv->auth_data); + } else { + ok = soup_auth_domain_try_generic_auth_callback ( + domain, msg, username); + } + + pw_free (password); + + if (ok) + return username; + else { + g_free (username); + return NULL; + } +} + +static char * +soup_auth_domain_basic_challenge (SoupAuthDomain *domain, SoupMessage *msg) +{ + GString *challenge; + + challenge = g_string_new ("Basic "); + soup_header_g_string_append_param (challenge, "realm", soup_auth_domain_get_realm (domain)); + return g_string_free (challenge, FALSE); +} + +static gboolean +soup_auth_domain_basic_check_password (SoupAuthDomain *domain, + SoupMessage *msg, + const char *username, + const char *password) +{ + const char *header; + char *msg_username, *msg_password; + gboolean ok; + + header = soup_message_headers_get_one (msg->request_headers, + "Authorization"); + if (!parse_basic (msg, header, &msg_username, &msg_password)) + return FALSE; + + ok = (!strcmp (username, msg_username) && + !strcmp (password, msg_password)); + g_free (msg_username); + pw_free (msg_password); + + return ok; +} + +static void +soup_auth_domain_basic_class_init (SoupAuthDomainBasicClass *basic_class) +{ + SoupAuthDomainClass *auth_domain_class = + SOUP_AUTH_DOMAIN_CLASS (basic_class); + GObjectClass *object_class = G_OBJECT_CLASS (basic_class); + + auth_domain_class->accepts = soup_auth_domain_basic_accepts; + auth_domain_class->challenge = soup_auth_domain_basic_challenge; + auth_domain_class->check_password = soup_auth_domain_basic_check_password; + + object_class->finalize = soup_auth_domain_basic_finalize; + object_class->set_property = soup_auth_domain_basic_set_property; + object_class->get_property = soup_auth_domain_basic_get_property; + + /** + * SOUP_AUTH_DOMAIN_BASIC_AUTH_CALLBACK: + * + * Alias for the #SoupAuthDomainBasic:auth-callback property. + * (The #SoupAuthDomainBasicAuthCallback.) + **/ + /** + * SoupAuthDomainBasic:auth-callback: (type SoupAuthDomainBasicAuthCallback) + * + * The #SoupAuthDomainBasicAuthCallback + */ + g_object_class_install_property ( + object_class, PROP_AUTH_CALLBACK, + g_param_spec_pointer (SOUP_AUTH_DOMAIN_BASIC_AUTH_CALLBACK, + "Authentication callback", + "Password-checking callback", + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); + /** + * SOUP_AUTH_DOMAIN_BASIC_AUTH_DATA: + * + * Alias for the #SoupAuthDomainBasic:auth-data property. + * (The data to pass to the #SoupAuthDomainBasicAuthCallback.) + **/ + /** + * SoupAuthDomainBasic:auth-data: + * + * The data to pass to the #SoupAuthDomainBasicAuthCallback + */ + g_object_class_install_property ( + object_class, PROP_AUTH_DATA, + g_param_spec_pointer (SOUP_AUTH_DOMAIN_BASIC_AUTH_DATA, + "Authentication callback data", + "Data to pass to authentication callback", + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); +} diff --git a/libsoup/server/soup-auth-domain-basic.h b/libsoup/server/soup-auth-domain-basic.h new file mode 100644 index 00000000..a260e891 --- /dev/null +++ b/libsoup/server/soup-auth-domain-basic.h @@ -0,0 +1,35 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2007 Novell, Inc. + */ + +#pragma once + +#include "soup-auth-domain.h" + +G_BEGIN_DECLS + +#define SOUP_TYPE_AUTH_DOMAIN_BASIC (soup_auth_domain_basic_get_type ()) +SOUP_AVAILABLE_IN_2_4 +G_DECLARE_FINAL_TYPE (SoupAuthDomainBasic, soup_auth_domain_basic, SOUP, AUTH_DOMAIN_BASIC, SoupAuthDomain) + +#define SOUP_AUTH_DOMAIN_BASIC_AUTH_CALLBACK "auth-callback" +#define SOUP_AUTH_DOMAIN_BASIC_AUTH_DATA "auth-data" + +SOUP_AVAILABLE_IN_2_4 +SoupAuthDomain *soup_auth_domain_basic_new (const char *optname1, + ...) G_GNUC_NULL_TERMINATED; + +typedef gboolean (*SoupAuthDomainBasicAuthCallback) (SoupAuthDomain *domain, + SoupMessage *msg, + const char *username, + const char *password, + gpointer user_data); + +SOUP_AVAILABLE_IN_2_4 +void soup_auth_domain_basic_set_auth_callback (SoupAuthDomain *domain, + SoupAuthDomainBasicAuthCallback callback, + gpointer user_data, + GDestroyNotify dnotify); + +G_END_DECLS diff --git a/libsoup/server/soup-auth-domain-digest.c b/libsoup/server/soup-auth-domain-digest.c new file mode 100644 index 00000000..ae9fa266 --- /dev/null +++ b/libsoup/server/soup-auth-domain-digest.c @@ -0,0 +1,454 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * soup-auth-domain-digest.c: HTTP Digest Authentication (server-side) + * + * Copyright (C) 2007 Novell, Inc. + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <string.h> +#include <stdlib.h> + +#include "soup-auth-domain-digest.h" +#include "soup.h" +#include "auth/soup-auth-digest-private.h" + +/** + * SECTION:soup-auth-domain-digest + * @short_description: Server-side "Digest" authentication + * + * #SoupAuthDomainDigest handles the server side of HTTP "Digest" + * authentication. + **/ + +enum { + PROP_0, + + PROP_AUTH_CALLBACK, + PROP_AUTH_DATA, + + LAST_PROP +}; + +struct _SoupAuthDomainDigest { + SoupAuthDomain parent; +}; + +typedef struct { + SoupAuthDomainDigestAuthCallback auth_callback; + gpointer auth_data; + GDestroyNotify auth_dnotify; + +} SoupAuthDomainDigestPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (SoupAuthDomainDigest, soup_auth_domain_digest, SOUP_TYPE_AUTH_DOMAIN) + +static void +soup_auth_domain_digest_init (SoupAuthDomainDigest *digest) +{ +} + +static void +soup_auth_domain_digest_finalize (GObject *object) +{ + SoupAuthDomainDigestPrivate *priv = + soup_auth_domain_digest_get_instance_private (SOUP_AUTH_DOMAIN_DIGEST (object)); + + if (priv->auth_dnotify) + priv->auth_dnotify (priv->auth_data); + + G_OBJECT_CLASS (soup_auth_domain_digest_parent_class)->finalize (object); +} + +static void +soup_auth_domain_digest_set_property (GObject *object, guint prop_id, + const GValue *value, GParamSpec *pspec) +{ + SoupAuthDomainDigestPrivate *priv = + soup_auth_domain_digest_get_instance_private (SOUP_AUTH_DOMAIN_DIGEST (object)); + + switch (prop_id) { + case PROP_AUTH_CALLBACK: + priv->auth_callback = g_value_get_pointer (value); + break; + case PROP_AUTH_DATA: + if (priv->auth_dnotify) { + priv->auth_dnotify (priv->auth_data); + priv->auth_dnotify = NULL; + } + priv->auth_data = g_value_get_pointer (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +soup_auth_domain_digest_get_property (GObject *object, guint prop_id, + GValue *value, GParamSpec *pspec) +{ + SoupAuthDomainDigestPrivate *priv = + soup_auth_domain_digest_get_instance_private (SOUP_AUTH_DOMAIN_DIGEST (object)); + + switch (prop_id) { + case PROP_AUTH_CALLBACK: + g_value_set_pointer (value, priv->auth_callback); + break; + case PROP_AUTH_DATA: + g_value_set_pointer (value, priv->auth_data); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +/** + * soup_auth_domain_digest_new: + * @optname1: name of first option, or %NULL + * @...: option name/value pairs + * + * Creates a #SoupAuthDomainDigest. You must set the + * %SOUP_AUTH_DOMAIN_REALM parameter, to indicate the realm name to be + * returned with the authentication challenge to the client. Other + * parameters are optional. + * + * Return value: the new #SoupAuthDomain + **/ +SoupAuthDomain * +soup_auth_domain_digest_new (const char *optname1, ...) +{ + SoupAuthDomain *domain; + va_list ap; + + va_start (ap, optname1); + domain = (SoupAuthDomain *)g_object_new_valist (SOUP_TYPE_AUTH_DOMAIN_DIGEST, + optname1, ap); + va_end (ap); + + g_return_val_if_fail (soup_auth_domain_get_realm (domain) != NULL, NULL); + + return domain; +} + +/** + * SoupAuthDomainDigestAuthCallback: + * @domain: (type SoupAuthDomainDigest): the domain + * @msg: the message being authenticated + * @username: the username provided by the client + * @user_data: the data passed to soup_auth_domain_digest_set_auth_callback() + * + * Callback used by #SoupAuthDomainDigest for authentication purposes. + * The application should look up @username in its password database, + * and return the corresponding encoded password (see + * soup_auth_domain_digest_encode_password()). + * + * Return value: (nullable): the encoded password, or %NULL if + * @username is not a valid user. @domain will free the password when + * it is done with it. + **/ + +/** + * soup_auth_domain_digest_set_auth_callback: + * @domain: (type SoupAuthDomainDigest): the domain + * @callback: the callback + * @user_data: data to pass to @auth_callback + * @dnotify: destroy notifier to free @user_data when @domain + * is destroyed + * + * Sets the callback that @domain will use to authenticate incoming + * requests. For each request containing authorization, @domain will + * invoke the callback, and then either accept or reject the request + * based on @callback's return value. + * + * You can also set the auth callback by setting the + * %SOUP_AUTH_DOMAIN_DIGEST_AUTH_CALLBACK and + * %SOUP_AUTH_DOMAIN_DIGEST_AUTH_DATA properties, which can also be + * used to set the callback at construct time. + **/ +void +soup_auth_domain_digest_set_auth_callback (SoupAuthDomain *domain, + SoupAuthDomainDigestAuthCallback callback, + gpointer user_data, + GDestroyNotify dnotify) +{ + SoupAuthDomainDigestPrivate *priv = + soup_auth_domain_digest_get_instance_private (SOUP_AUTH_DOMAIN_DIGEST (domain)); + + if (priv->auth_dnotify) + priv->auth_dnotify (priv->auth_data); + + priv->auth_callback = callback; + priv->auth_data = user_data; + priv->auth_dnotify = dnotify; + + g_object_notify (G_OBJECT (domain), SOUP_AUTH_DOMAIN_DIGEST_AUTH_CALLBACK); + g_object_notify (G_OBJECT (domain), SOUP_AUTH_DOMAIN_DIGEST_AUTH_DATA); +} + +static gboolean +check_hex_urp (SoupAuthDomain *domain, SoupMessage *msg, + GHashTable *params, const char *username, + const char *hex_urp) +{ + const char *uri, *qop, *realm, *msg_username; + const char *nonce, *nc, *cnonce, *response; + char hex_a1[33], computed_response[33]; + int nonce_count; + SoupURI *dig_uri, *req_uri; + + msg_username = g_hash_table_lookup (params, "username"); + if (!msg_username || strcmp (msg_username, username) != 0) + return FALSE; + + /* Check uri */ + uri = g_hash_table_lookup (params, "uri"); + if (!uri) + return FALSE; + + req_uri = soup_message_get_uri (msg); + dig_uri = soup_uri_new (uri); + if (dig_uri) { + if (!soup_uri_equal (dig_uri, req_uri)) { + soup_uri_free (dig_uri); + return FALSE; + } + soup_uri_free (dig_uri); + } else { + char *req_path; + char *dig_path; + + req_path = soup_uri_to_string (req_uri, TRUE); + dig_path = soup_uri_decode (uri); + + if (strcmp (dig_path, req_path) != 0) { + g_free (req_path); + g_free (dig_path); + return FALSE; + } + g_free (req_path); + g_free (dig_path); + } + + /* Check qop; we only support "auth" for now */ + qop = g_hash_table_lookup (params, "qop"); + if (!qop || strcmp (qop, "auth") != 0) + return FALSE; + + /* Check realm */ + realm = g_hash_table_lookup (params, "realm"); + if (!realm || strcmp (realm, soup_auth_domain_get_realm (domain)) != 0) + return FALSE; + + nonce = g_hash_table_lookup (params, "nonce"); + if (!nonce) + return FALSE; + nc = g_hash_table_lookup (params, "nc"); + if (!nc) + return FALSE; + nonce_count = strtoul (nc, NULL, 16); + if (nonce_count <= 0) + return FALSE; + cnonce = g_hash_table_lookup (params, "cnonce"); + if (!cnonce) + return FALSE; + response = g_hash_table_lookup (params, "response"); + if (!response) + return FALSE; + + soup_auth_digest_compute_hex_a1 (hex_urp, + SOUP_AUTH_DIGEST_ALGORITHM_MD5, + nonce, cnonce, hex_a1); + soup_auth_digest_compute_response (msg->method, uri, + hex_a1, + SOUP_AUTH_DIGEST_QOP_AUTH, + nonce, cnonce, nonce_count, + computed_response); + return strcmp (response, computed_response) == 0; +} + +static char * +soup_auth_domain_digest_accepts (SoupAuthDomain *domain, SoupMessage *msg, + const char *header) +{ + SoupAuthDomainDigestPrivate *priv = + soup_auth_domain_digest_get_instance_private (SOUP_AUTH_DOMAIN_DIGEST (domain)); + GHashTable *params; + const char *username; + gboolean accept = FALSE; + char *ret_user; + + if (strncmp (header, "Digest ", 7) != 0) + return NULL; + + params = soup_header_parse_param_list (header + 7); + if (!params) + return NULL; + + username = g_hash_table_lookup (params, "username"); + if (!username) { + soup_header_free_param_list (params); + return NULL; + } + + if (priv->auth_callback) { + char *hex_urp; + + hex_urp = priv->auth_callback (domain, msg, username, + priv->auth_data); + if (hex_urp) { + accept = check_hex_urp (domain, msg, params, + username, hex_urp); + g_free (hex_urp); + } else + accept = FALSE; + } else { + accept = soup_auth_domain_try_generic_auth_callback ( + domain, msg, username); + } + + ret_user = accept ? g_strdup (username) : NULL; + soup_header_free_param_list (params); + return ret_user; +} + +static char * +soup_auth_domain_digest_challenge (SoupAuthDomain *domain, SoupMessage *msg) +{ + GString *str; + + str = g_string_new ("Digest "); + soup_header_g_string_append_param_quoted (str, "realm", soup_auth_domain_get_realm (domain)); + g_string_append_printf (str, ", nonce=\"%lu%lu\"", + (unsigned long) msg, + (unsigned long) time (0)); + g_string_append_printf (str, ", qop=\"auth\""); + g_string_append_printf (str, ", algorithm=MD5"); + + return g_string_free (str, FALSE); +} + +/** + * soup_auth_domain_digest_encode_password: + * @username: a username + * @realm: an auth realm name + * @password: the password for @username in @realm + * + * Encodes the username/realm/password triplet for Digest + * authentication. (That is, it returns a stringified MD5 hash of + * @username, @realm, and @password concatenated together). This is + * the form that is needed as the return value of + * #SoupAuthDomainDigest's auth handler. + * + * For security reasons, you should store the encoded hash, rather + * than storing the cleartext password itself and calling this method + * only when you need to verify it. This way, if your server is + * compromised, the attackers will not gain access to cleartext + * passwords which might also be usable at other sites. (Note also + * that the encoded password returned by this method is identical to + * the encoded password stored in an Apache .htdigest file.) + * + * Return value: the encoded password + **/ +char * +soup_auth_domain_digest_encode_password (const char *username, + const char *realm, + const char *password) +{ + char hex_urp[33]; + + soup_auth_digest_compute_hex_urp (username, realm, password, hex_urp); + return g_strdup (hex_urp); +} + +static gboolean +soup_auth_domain_digest_check_password (SoupAuthDomain *domain, + SoupMessage *msg, + const char *username, + const char *password) +{ + const char *header; + GHashTable *params; + const char *msg_username; + char hex_urp[33]; + gboolean accept; + + header = soup_message_headers_get_one (msg->request_headers, + "Authorization"); + if (!header || (strncmp (header, "Digest ", 7) != 0)) + return FALSE; + + params = soup_header_parse_param_list (header + 7); + if (!params) + return FALSE; + + msg_username = g_hash_table_lookup (params, "username"); + if (!msg_username || strcmp (msg_username, username) != 0) { + soup_header_free_param_list (params); + return FALSE; + } + + soup_auth_digest_compute_hex_urp (username, + soup_auth_domain_get_realm (domain), + password, hex_urp); + accept = check_hex_urp (domain, msg, params, username, hex_urp); + soup_header_free_param_list (params); + return accept; +} + +static void +soup_auth_domain_digest_class_init (SoupAuthDomainDigestClass *digest_class) +{ + SoupAuthDomainClass *auth_domain_class = + SOUP_AUTH_DOMAIN_CLASS (digest_class); + GObjectClass *object_class = G_OBJECT_CLASS (digest_class); + + auth_domain_class->accepts = soup_auth_domain_digest_accepts; + auth_domain_class->challenge = soup_auth_domain_digest_challenge; + auth_domain_class->check_password = soup_auth_domain_digest_check_password; + + object_class->finalize = soup_auth_domain_digest_finalize; + object_class->set_property = soup_auth_domain_digest_set_property; + object_class->get_property = soup_auth_domain_digest_get_property; + + /** + * SOUP_AUTH_DOMAIN_DIGEST_AUTH_CALLBACK: + * + * Alias for the #SoupAuthDomainDigest:auth-callback property. + * (The #SoupAuthDomainDigestAuthCallback.) + **/ + /** + * SoupAuthDomainDigest:auth-callback: (type SoupAuthDomainDigestAuthCallback) + * + * The #SoupAuthDomainDigestAuthCallback + */ + g_object_class_install_property ( + object_class, PROP_AUTH_CALLBACK, + g_param_spec_pointer (SOUP_AUTH_DOMAIN_DIGEST_AUTH_CALLBACK, + "Authentication callback", + "Password-finding callback", + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); + /** + * SOUP_AUTH_DOMAIN_DIGEST_AUTH_DATA: + * + * Alias for the #SoupAuthDomainDigest:auth-callback property. + * (The #SoupAuthDomainDigestAuthCallback.) + **/ + /** + * SoupAuthDomainDigest:auth-data: + * + * The data to pass to the #SoupAuthDomainDigestAuthCallback + */ + g_object_class_install_property ( + object_class, PROP_AUTH_DATA, + g_param_spec_pointer (SOUP_AUTH_DOMAIN_DIGEST_AUTH_DATA, + "Authentication callback data", + "Data to pass to authentication callback", + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); +} diff --git a/libsoup/server/soup-auth-domain-digest.h b/libsoup/server/soup-auth-domain-digest.h new file mode 100644 index 00000000..523497ce --- /dev/null +++ b/libsoup/server/soup-auth-domain-digest.h @@ -0,0 +1,39 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2007 Novell, Inc. + */ + +#pragma once + +#include "soup-auth-domain.h" + +G_BEGIN_DECLS + +#define SOUP_TYPE_AUTH_DOMAIN_DIGEST (soup_auth_domain_digest_get_type ()) +SOUP_AVAILABLE_IN_2_4 +G_DECLARE_FINAL_TYPE (SoupAuthDomainDigest, soup_auth_domain_digest, SOUP, AUTH_DOMAIN_DIGEST, SoupAuthDomain) + +#define SOUP_AUTH_DOMAIN_DIGEST_AUTH_CALLBACK "auth-callback" +#define SOUP_AUTH_DOMAIN_DIGEST_AUTH_DATA "auth-data" + +SOUP_AVAILABLE_IN_2_4 +SoupAuthDomain *soup_auth_domain_digest_new (const char *optname1, + ...) G_GNUC_NULL_TERMINATED; + +typedef char * (*SoupAuthDomainDigestAuthCallback) (SoupAuthDomain *domain, + SoupMessage *msg, + const char *username, + gpointer user_data); + +SOUP_AVAILABLE_IN_2_4 +void soup_auth_domain_digest_set_auth_callback (SoupAuthDomain *domain, + SoupAuthDomainDigestAuthCallback callback, + gpointer user_data, + GDestroyNotify dnotify); + +SOUP_AVAILABLE_IN_2_4 +char *soup_auth_domain_digest_encode_password (const char *username, + const char *realm, + const char *password); + +G_END_DECLS diff --git a/libsoup/server/soup-auth-domain.c b/libsoup/server/soup-auth-domain.c new file mode 100644 index 00000000..8785c474 --- /dev/null +++ b/libsoup/server/soup-auth-domain.c @@ -0,0 +1,635 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * soup-auth-domain.c: HTTP Authentication Domain (server-side) + * + * Copyright (C) 2007 Novell, Inc. + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <string.h> + +#include "soup-auth-domain.h" +#include "soup.h" +#include "soup-path-map.h" + +/** + * SECTION:soup-auth-domain + * @short_description: Server-side authentication + * @see_also: #SoupServer + * + * A #SoupAuthDomain manages authentication for all or part of a + * #SoupServer. To make a server require authentication, first create + * an appropriate subclass of #SoupAuthDomain, and then add it to the + * server with soup_server_add_auth_domain(). + * + * In order for an auth domain to have any effect, you must add one or + * more paths to it (via soup_auth_domain_add_path() or the + * %SOUP_AUTH_DOMAIN_ADD_PATH property). To require authentication for + * all ordinary requests, add the path "/". (Note that this does not + * include the special "*" URI (eg, "OPTIONS *"), which must be added + * as a separate path if you want to cover it.) + * + * If you need greater control over which requests should and + * shouldn't be authenticated, add paths covering everything you + * <emphasis>might</emphasis> want authenticated, and then use a + * filter (soup_auth_domain_set_filter()) to bypass authentication for + * those requests that don't need it. + **/ + +enum { + PROP_0, + + PROP_REALM, + PROP_PROXY, + PROP_ADD_PATH, + PROP_REMOVE_PATH, + PROP_FILTER, + PROP_FILTER_DATA, + PROP_GENERIC_AUTH_CALLBACK, + PROP_GENERIC_AUTH_DATA, + + LAST_PROP +}; + +typedef struct { + char *realm; + gboolean proxy; + SoupPathMap *paths; + + SoupAuthDomainFilter filter; + gpointer filter_data; + GDestroyNotify filter_dnotify; + + SoupAuthDomainGenericAuthCallback auth_callback; + gpointer auth_data; + GDestroyNotify auth_dnotify; + +} SoupAuthDomainPrivate; + +G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (SoupAuthDomain, soup_auth_domain, G_TYPE_OBJECT) + +static void +soup_auth_domain_init (SoupAuthDomain *domain) +{ + SoupAuthDomainPrivate *priv = soup_auth_domain_get_instance_private (domain); + + priv->paths = soup_path_map_new (NULL); +} + +static void +soup_auth_domain_finalize (GObject *object) +{ + SoupAuthDomainPrivate *priv = soup_auth_domain_get_instance_private (SOUP_AUTH_DOMAIN (object)); + + g_free (priv->realm); + soup_path_map_free (priv->paths); + + if (priv->filter_dnotify) + priv->filter_dnotify (priv->filter_data); + if (priv->auth_dnotify) + priv->auth_dnotify (priv->auth_data); + + G_OBJECT_CLASS (soup_auth_domain_parent_class)->finalize (object); +} + +static void +soup_auth_domain_set_property (GObject *object, guint prop_id, + const GValue *value, GParamSpec *pspec) +{ + SoupAuthDomain *auth_domain = SOUP_AUTH_DOMAIN (object); + SoupAuthDomainPrivate *priv = soup_auth_domain_get_instance_private (auth_domain); + + switch (prop_id) { + case PROP_REALM: + g_free (priv->realm); + priv->realm = g_value_dup_string (value); + break; + case PROP_PROXY: + priv->proxy = g_value_get_boolean (value); + break; + case PROP_ADD_PATH: + soup_auth_domain_add_path (auth_domain, + g_value_get_string (value)); + break; + case PROP_REMOVE_PATH: + soup_auth_domain_remove_path (auth_domain, + g_value_get_string (value)); + break; + case PROP_FILTER: + priv->filter = g_value_get_pointer (value); + break; + case PROP_FILTER_DATA: + if (priv->filter_dnotify) { + priv->filter_dnotify (priv->filter_data); + priv->filter_dnotify = NULL; + } + priv->filter_data = g_value_get_pointer (value); + break; + case PROP_GENERIC_AUTH_CALLBACK: + priv->auth_callback = g_value_get_pointer (value); + break; + case PROP_GENERIC_AUTH_DATA: + if (priv->auth_dnotify) { + priv->auth_dnotify (priv->auth_data); + priv->auth_dnotify = NULL; + } + priv->auth_data = g_value_get_pointer (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +soup_auth_domain_get_property (GObject *object, guint prop_id, + GValue *value, GParamSpec *pspec) +{ + SoupAuthDomainPrivate *priv = soup_auth_domain_get_instance_private (SOUP_AUTH_DOMAIN (object)); + + switch (prop_id) { + case PROP_REALM: + g_value_set_string (value, priv->realm); + break; + case PROP_PROXY: + g_value_set_boolean (value, priv->proxy); + break; + case PROP_FILTER: + g_value_set_pointer (value, priv->filter); + break; + case PROP_FILTER_DATA: + g_value_set_pointer (value, priv->filter_data); + break; + case PROP_GENERIC_AUTH_CALLBACK: + g_value_set_pointer (value, priv->auth_callback); + break; + case PROP_GENERIC_AUTH_DATA: + g_value_set_pointer (value, priv->auth_data); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +soup_auth_domain_class_init (SoupAuthDomainClass *auth_domain_class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (auth_domain_class); + + object_class->finalize = soup_auth_domain_finalize; + object_class->set_property = soup_auth_domain_set_property; + object_class->get_property = soup_auth_domain_get_property; + + /** + * SOUP_AUTH_DOMAIN_REALM: + * + * Alias for the #SoupAuthDomain:realm property. (The realm of + * this auth domain.) + **/ + g_object_class_install_property ( + object_class, PROP_REALM, + g_param_spec_string (SOUP_AUTH_DOMAIN_REALM, + "Realm", + "The realm of this auth domain", + NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + /** + * SOUP_AUTH_DOMAIN_PROXY: + * + * Alias for the #SoupAuthDomain:proxy property. (Whether or + * not this is a proxy auth domain.) + **/ + g_object_class_install_property ( + object_class, PROP_PROXY, + g_param_spec_boolean (SOUP_AUTH_DOMAIN_PROXY, + "Proxy", + "Whether or not this is a proxy auth domain", + FALSE, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + /** + * SOUP_AUTH_DOMAIN_ADD_PATH: + * + * Alias for the #SoupAuthDomain:add-path property. (Shortcut + * for calling soup_auth_domain_add_path().) + **/ + g_object_class_install_property ( + object_class, PROP_ADD_PATH, + g_param_spec_string (SOUP_AUTH_DOMAIN_ADD_PATH, + "Add a path", + "Add a path covered by this auth domain", + NULL, + G_PARAM_WRITABLE | + G_PARAM_STATIC_STRINGS)); + /** + * SOUP_AUTH_DOMAIN_REMOVE_PATH: + * + * Alias for the #SoupAuthDomain:remove-path property. + * (Shortcut for calling soup_auth_domain_remove_path().) + **/ + g_object_class_install_property ( + object_class, PROP_REMOVE_PATH, + g_param_spec_string (SOUP_AUTH_DOMAIN_REMOVE_PATH, + "Remove a path", + "Remove a path covered by this auth domain", + NULL, + G_PARAM_WRITABLE | + G_PARAM_STATIC_STRINGS)); + /** + * SOUP_AUTH_DOMAIN_FILTER: + * + * Alias for the #SoupAuthDomain:filter property. (The + * #SoupAuthDomainFilter for the domain.) + **/ + /** + * SoupAuthDomain:filter: (type SoupAuthDomainFilter) + * + * The #SoupAuthDomainFilter for the domain + */ + g_object_class_install_property ( + object_class, PROP_FILTER, + g_param_spec_pointer (SOUP_AUTH_DOMAIN_FILTER, + "Filter", + "A filter for deciding whether or not to require authentication", + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); + /** + * SOUP_AUTH_DOMAIN_FILTER_DATA: + * + * Alias for the #SoupAuthDomain:filter-data property. (Data + * to pass to the #SoupAuthDomainFilter.) + **/ + /** + * SoupAuthDomain:generic-auth-callback: (type SoupAuthDomainGenericAuthCallback) + * + * The #SoupAuthDomainGenericAuthCallback for the domain + */ + g_object_class_install_property ( + object_class, PROP_FILTER_DATA, + g_param_spec_pointer (SOUP_AUTH_DOMAIN_FILTER_DATA, + "Filter data", + "Data to pass to filter", + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); + /** + * SOUP_AUTH_DOMAIN_GENERIC_AUTH_CALLBACK: + * + * Alias for the #SoupAuthDomain:generic-auth-callback property. + * (The #SoupAuthDomainGenericAuthCallback.) + **/ + g_object_class_install_property ( + object_class, PROP_GENERIC_AUTH_CALLBACK, + g_param_spec_pointer (SOUP_AUTH_DOMAIN_GENERIC_AUTH_CALLBACK, + "Generic authentication callback", + "An authentication callback that can be used with any SoupAuthDomain subclass", + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); + /** + * SOUP_AUTH_DOMAIN_GENERIC_AUTH_DATA: + * + * Alias for the #SoupAuthDomain:generic-auth-data property. + * (The data to pass to the #SoupAuthDomainGenericAuthCallback.) + **/ + g_object_class_install_property ( + object_class, PROP_GENERIC_AUTH_DATA, + g_param_spec_pointer (SOUP_AUTH_DOMAIN_GENERIC_AUTH_DATA, + "Authentication callback data", + "Data to pass to auth callback", + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); +} + +/** + * soup_auth_domain_add_path: + * @domain: a #SoupAuthDomain + * @path: the path to add to @domain + * + * Adds @path to @domain, such that requests under @path on @domain's + * server will require authentication (unless overridden by + * soup_auth_domain_remove_path() or soup_auth_domain_set_filter()). + * + * You can also add paths by setting the %SOUP_AUTH_DOMAIN_ADD_PATH + * property, which can also be used to add one or more paths at + * construct time. + **/ +void +soup_auth_domain_add_path (SoupAuthDomain *domain, const char *path) +{ + SoupAuthDomainPrivate *priv = soup_auth_domain_get_instance_private (domain); + + /* "" should not match "*" */ + if (!*path) + path = "/"; + + soup_path_map_add (priv->paths, path, GINT_TO_POINTER (TRUE)); +} + +/** + * soup_auth_domain_remove_path: + * @domain: a #SoupAuthDomain + * @path: the path to remove from @domain + * + * Removes @path from @domain, such that requests under @path on + * @domain's server will NOT require authentication. + * + * This is not simply an undo-er for soup_auth_domain_add_path(); it + * can be used to "carve out" a subtree that does not require + * authentication inside a hierarchy that does. Note also that unlike + * with soup_auth_domain_add_path(), this cannot be overridden by + * adding a filter, as filters can only bypass authentication that + * would otherwise be required, not require it where it would + * otherwise be unnecessary. + * + * You can also remove paths by setting the + * %SOUP_AUTH_DOMAIN_REMOVE_PATH property, which can also be used to + * remove one or more paths at construct time. + **/ +void +soup_auth_domain_remove_path (SoupAuthDomain *domain, const char *path) +{ + SoupAuthDomainPrivate *priv = soup_auth_domain_get_instance_private (domain); + + /* "" should not match "*" */ + if (!*path) + path = "/"; + + soup_path_map_add (priv->paths, path, GINT_TO_POINTER (FALSE)); +} + +/** + * SoupAuthDomainFilter: + * @domain: a #SoupAuthDomain + * @msg: a #SoupMessage + * @user_data: the data passed to soup_auth_domain_set_filter() + * + * The prototype for a #SoupAuthDomain filter; see + * soup_auth_domain_set_filter() for details. + * + * Return value: %TRUE if @msg requires authentication, %FALSE if not. + **/ + +/** + * soup_auth_domain_set_filter: + * @domain: a #SoupAuthDomain + * @filter: the auth filter for @domain + * @filter_data: data to pass to @filter + * @dnotify: destroy notifier to free @filter_data when @domain + * is destroyed + * + * Adds @filter as an authentication filter to @domain. The filter + * gets a chance to bypass authentication for certain requests that + * would otherwise require it. Eg, it might check the message's path + * in some way that is too complicated to do via the other methods, or + * it might check the message's method, and allow GETs but not PUTs. + * + * The filter function returns %TRUE if the request should still + * require authentication, or %FALSE if authentication is unnecessary + * for this request. + * + * To help prevent security holes, your filter should return %TRUE by + * default, and only return %FALSE under specifically-tested + * circumstances, rather than the other way around. Eg, in the example + * above, where you want to authenticate PUTs but not GETs, you should + * check if the method is GET and return %FALSE in that case, and then + * return %TRUE for all other methods (rather than returning %TRUE for + * PUT and %FALSE for all other methods). This way if it turned out + * (now or later) that some paths supported additional methods besides + * GET and PUT, those methods would default to being NOT allowed for + * unauthenticated users. + * + * You can also set the filter by setting the %SOUP_AUTH_DOMAIN_FILTER + * and %SOUP_AUTH_DOMAIN_FILTER_DATA properties, which can also be + * used to set the filter at construct time. + **/ +void +soup_auth_domain_set_filter (SoupAuthDomain *domain, + SoupAuthDomainFilter filter, + gpointer filter_data, + GDestroyNotify dnotify) +{ + SoupAuthDomainPrivate *priv = soup_auth_domain_get_instance_private (domain); + + if (priv->filter_dnotify) + priv->filter_dnotify (priv->filter_data); + + priv->filter = filter; + priv->filter_data = filter_data; + priv->filter_dnotify = dnotify; + + g_object_notify (G_OBJECT (domain), SOUP_AUTH_DOMAIN_FILTER); + g_object_notify (G_OBJECT (domain), SOUP_AUTH_DOMAIN_FILTER_DATA); +} + +/** + * soup_auth_domain_get_realm: + * @domain: a #SoupAuthDomain + * + * Gets the realm name associated with @domain + * + * Return value: @domain's realm + **/ +const char * +soup_auth_domain_get_realm (SoupAuthDomain *domain) +{ + SoupAuthDomainPrivate *priv = soup_auth_domain_get_instance_private (domain); + + return priv->realm; +} + +/** + * SoupAuthDomainGenericAuthCallback: + * @domain: a #SoupAuthDomain + * @msg: the #SoupMessage being authenticated + * @username: the username from @msg + * @user_data: the data passed to + * soup_auth_domain_set_generic_auth_callback() + * + * The prototype for a #SoupAuthDomain generic authentication callback. + * + * The callback should look up the user's password, call + * soup_auth_domain_check_password(), and use the return value from + * that method as its own return value. + * + * In general, for security reasons, it is preferable to use the + * auth-domain-specific auth callbacks (eg, + * #SoupAuthDomainBasicAuthCallback and + * #SoupAuthDomainDigestAuthCallback), because they don't require + * keeping a cleartext password database. Most users will use the same + * password for many different sites, meaning if any site with a + * cleartext password database is compromised, accounts on other + * servers might be compromised as well. For many of the cases where + * #SoupServer is used, this is not really relevant, but it may still + * be worth considering. + * + * Return value: %TRUE if @msg is authenticated, %FALSE if not. + **/ + +/** + * soup_auth_domain_set_generic_auth_callback: + * @domain: a #SoupAuthDomain + * @auth_callback: the auth callback + * @auth_data: data to pass to @auth_callback + * @dnotify: destroy notifier to free @auth_data when @domain + * is destroyed + * + * Sets @auth_callback as an authentication-handling callback for + * @domain. Whenever a request comes in to @domain which cannot be + * authenticated via a domain-specific auth callback (eg, + * #SoupAuthDomainDigestAuthCallback), the generic auth callback + * will be invoked. See #SoupAuthDomainGenericAuthCallback for information + * on what the callback should do. + **/ +void +soup_auth_domain_set_generic_auth_callback (SoupAuthDomain *domain, + SoupAuthDomainGenericAuthCallback auth_callback, + gpointer auth_data, + GDestroyNotify dnotify) +{ + SoupAuthDomainPrivate *priv = soup_auth_domain_get_instance_private (domain); + + if (priv->auth_dnotify) + priv->auth_dnotify (priv->auth_data); + + priv->auth_callback = auth_callback; + priv->auth_data = auth_data; + priv->auth_dnotify = dnotify; + + g_object_notify (G_OBJECT (domain), SOUP_AUTH_DOMAIN_GENERIC_AUTH_CALLBACK); + g_object_notify (G_OBJECT (domain), SOUP_AUTH_DOMAIN_GENERIC_AUTH_DATA); +} + +gboolean +soup_auth_domain_try_generic_auth_callback (SoupAuthDomain *domain, + SoupMessage *msg, + const char *username) +{ + SoupAuthDomainPrivate *priv = soup_auth_domain_get_instance_private (domain); + + if (priv->auth_callback) + return priv->auth_callback (domain, msg, username, priv->auth_data); + else + return FALSE; +} + +/** + * soup_auth_domain_check_password: + * @domain: a #SoupAuthDomain + * @msg: a #SoupMessage + * @username: a username + * @password: a password + * + * Checks if @msg authenticates to @domain via @username and + * @password. This would normally be called from a + * #SoupAuthDomainGenericAuthCallback. + * + * Return value: whether or not the message is authenticated + **/ +gboolean +soup_auth_domain_check_password (SoupAuthDomain *domain, + SoupMessage *msg, + const char *username, + const char *password) +{ + return SOUP_AUTH_DOMAIN_GET_CLASS (domain)->check_password (domain, msg, + username, + password); +} + +/** + * soup_auth_domain_covers: + * @domain: a #SoupAuthDomain + * @msg: a #SoupMessage + * + * Checks if @domain requires @msg to be authenticated (according to + * its paths and filter function). This does not actually look at + * whether @msg <emphasis>is</emphasis> authenticated, merely whether + * or not it needs to be. + * + * This is used by #SoupServer internally and is probably of no use to + * anyone else. + * + * Return value: %TRUE if @domain requires @msg to be authenticated + **/ +gboolean +soup_auth_domain_covers (SoupAuthDomain *domain, SoupMessage *msg) +{ + SoupAuthDomainPrivate *priv = soup_auth_domain_get_instance_private (domain); + const char *path; + + if (!priv->proxy) { + path = soup_message_get_uri (msg)->path; + if (!soup_path_map_lookup (priv->paths, path)) + return FALSE; + } + + if (priv->filter && !priv->filter (domain, msg, priv->filter_data)) + return FALSE; + else + return TRUE; +} + +/** + * soup_auth_domain_accepts: + * @domain: a #SoupAuthDomain + * @msg: a #SoupMessage + * + * Checks if @msg contains appropriate authorization for @domain to + * accept it. Mirroring soup_auth_domain_covers(), this does not check + * whether or not @domain <emphasis>cares</emphasis> if @msg is + * authorized. + * + * This is used by #SoupServer internally and is probably of no use to + * anyone else. + * + * Return value: (nullable): the username that @msg has authenticated + * as, if in fact it has authenticated. %NULL otherwise. + **/ +char * +soup_auth_domain_accepts (SoupAuthDomain *domain, SoupMessage *msg) +{ + SoupAuthDomainPrivate *priv = soup_auth_domain_get_instance_private (domain); + const char *header; + + header = soup_message_headers_get_one (msg->request_headers, + priv->proxy ? + "Proxy-Authorization" : + "Authorization"); + if (!header) + return NULL; + return SOUP_AUTH_DOMAIN_GET_CLASS (domain)->accepts (domain, msg, header); +} + +/** + * soup_auth_domain_challenge: (virtual challenge) + * @domain: a #SoupAuthDomain + * @msg: a #SoupMessage + * + * Adds a "WWW-Authenticate" or "Proxy-Authenticate" header to @msg, + * requesting that the client authenticate, and sets @msg's status + * accordingly. + * + * This is used by #SoupServer internally and is probably of no use to + * anyone else. + **/ +void +soup_auth_domain_challenge (SoupAuthDomain *domain, SoupMessage *msg) +{ + SoupAuthDomainPrivate *priv = soup_auth_domain_get_instance_private (domain); + char *challenge; + + challenge = SOUP_AUTH_DOMAIN_GET_CLASS (domain)->challenge (domain, msg); + soup_message_set_status (msg, priv->proxy ? + SOUP_STATUS_PROXY_UNAUTHORIZED : + SOUP_STATUS_UNAUTHORIZED); + soup_message_headers_append (msg->response_headers, + priv->proxy ? + "Proxy-Authenticate" : + "WWW-Authenticate", + challenge); + g_free (challenge); +} diff --git a/libsoup/server/soup-auth-domain.h b/libsoup/server/soup-auth-domain.h new file mode 100644 index 00000000..397dc53b --- /dev/null +++ b/libsoup/server/soup-auth-domain.h @@ -0,0 +1,92 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2007 Novell, Inc. + */ + +#pragma once + +#include "soup-types.h" + +G_BEGIN_DECLS + +#define SOUP_TYPE_AUTH_DOMAIN (soup_auth_domain_get_type ()) +SOUP_AVAILABLE_IN_2_4 +G_DECLARE_DERIVABLE_TYPE (SoupAuthDomain, soup_auth_domain, SOUP, AUTH_DOMAIN, GObject) + +struct _SoupAuthDomainClass { + GObjectClass parent_class; + + char * (*accepts) (SoupAuthDomain *domain, + SoupMessage *msg, + const char *header); + char * (*challenge) (SoupAuthDomain *domain, + SoupMessage *msg); + gboolean (*check_password) (SoupAuthDomain *domain, + SoupMessage *msg, + const char *username, + const char *password); + gpointer padding[6]; +}; + +#define SOUP_AUTH_DOMAIN_REALM "realm" +#define SOUP_AUTH_DOMAIN_PROXY "proxy" +#define SOUP_AUTH_DOMAIN_ADD_PATH "add-path" +#define SOUP_AUTH_DOMAIN_REMOVE_PATH "remove-path" +#define SOUP_AUTH_DOMAIN_FILTER "filter" +#define SOUP_AUTH_DOMAIN_FILTER_DATA "filter-data" +#define SOUP_AUTH_DOMAIN_GENERIC_AUTH_CALLBACK "generic-auth-callback" +#define SOUP_AUTH_DOMAIN_GENERIC_AUTH_DATA "generic-auth-data" + +typedef gboolean (*SoupAuthDomainFilter) (SoupAuthDomain *domain, + SoupMessage *msg, + gpointer user_data); + +typedef gboolean (*SoupAuthDomainGenericAuthCallback) (SoupAuthDomain *domain, + SoupMessage *msg, + const char *username, + gpointer user_data); + +SOUP_AVAILABLE_IN_2_4 +void soup_auth_domain_add_path (SoupAuthDomain *domain, + const char *path); +SOUP_AVAILABLE_IN_2_4 +void soup_auth_domain_remove_path (SoupAuthDomain *domain, + const char *path); + +SOUP_AVAILABLE_IN_2_4 +void soup_auth_domain_set_filter (SoupAuthDomain *domain, + SoupAuthDomainFilter filter, + gpointer filter_data, + GDestroyNotify dnotify); + +SOUP_AVAILABLE_IN_2_4 +const char *soup_auth_domain_get_realm (SoupAuthDomain *domain); + +SOUP_AVAILABLE_IN_2_4 +void soup_auth_domain_set_generic_auth_callback (SoupAuthDomain *domain, + SoupAuthDomainGenericAuthCallback auth_callback, + gpointer auth_data, + GDestroyNotify dnotify); +SOUP_AVAILABLE_IN_2_4 +gboolean soup_auth_domain_check_password (SoupAuthDomain *domain, + SoupMessage *msg, + const char *username, + const char *password); + +SOUP_AVAILABLE_IN_2_4 +gboolean soup_auth_domain_covers (SoupAuthDomain *domain, + SoupMessage *msg); +SOUP_AVAILABLE_IN_2_4 +char *soup_auth_domain_accepts (SoupAuthDomain *domain, + SoupMessage *msg); +SOUP_AVAILABLE_IN_2_4 +void soup_auth_domain_challenge (SoupAuthDomain *domain, + SoupMessage *msg); + +/* protected */ +SOUP_AVAILABLE_IN_2_4 +gboolean soup_auth_domain_try_generic_auth_callback (SoupAuthDomain *domain, + SoupMessage *msg, + const char *username); + +G_END_DECLS diff --git a/libsoup/server/soup-message-server-io.c b/libsoup/server/soup-message-server-io.c new file mode 100644 index 00000000..acd1490c --- /dev/null +++ b/libsoup/server/soup-message-server-io.c @@ -0,0 +1,304 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * soup-message-server-io.c: server-side request/response + * + * Copyright (C) 2000-2003, Ximian, Inc. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include <stdlib.h> +#include <string.h> + +#include <glib/gi18n-lib.h> + +#include "soup.h" +#include "soup-message-private.h" +#include "soup-misc.h" +#include "soup-socket-private.h" + +static SoupURI * +parse_connect_authority (const char *req_path) +{ + SoupURI *uri; + char *fake_uri; + + fake_uri = g_strdup_printf ("http://%s", req_path); + uri = soup_uri_new (fake_uri); + g_free (fake_uri); + + if (uri->user || uri->password || + uri->query || uri->fragment || + !uri->host || + (uri->port == 0) || + (strcmp (uri->path, "/") != 0)) { + soup_uri_free (uri); + return NULL; + } + + return uri; +} + +static guint +parse_request_headers (SoupMessage *msg, char *headers, guint headers_len, + SoupEncoding *encoding, gpointer sock, GError **error) +{ + char *req_method, *req_path, *url; + SoupHTTPVersion version; + const char *req_host; + guint status; + SoupURI *uri; + + status = soup_headers_parse_request (headers, headers_len, + msg->request_headers, + &req_method, + &req_path, + &version); + if (!SOUP_STATUS_IS_SUCCESSFUL (status)) { + if (status == SOUP_STATUS_MALFORMED) { + g_set_error_literal (error, SOUP_REQUEST_ERROR, + SOUP_REQUEST_ERROR_PARSING, + _("Could not parse HTTP request")); + } + return status; + } + + g_object_set (G_OBJECT (msg), + SOUP_MESSAGE_METHOD, req_method, + SOUP_MESSAGE_HTTP_VERSION, version, + NULL); + g_free (req_method); + + /* Handle request body encoding */ + *encoding = soup_message_headers_get_encoding (msg->request_headers); + if (*encoding == SOUP_ENCODING_UNRECOGNIZED) { + if (soup_message_headers_get_list (msg->request_headers, "Transfer-Encoding")) + return SOUP_STATUS_NOT_IMPLEMENTED; + else + return SOUP_STATUS_BAD_REQUEST; + } + + /* Generate correct context for request */ + req_host = soup_message_headers_get_one (msg->request_headers, "Host"); + if (req_host && strchr (req_host, '/')) { + g_free (req_path); + return SOUP_STATUS_BAD_REQUEST; + } + + if (!strcmp (req_path, "*") && req_host) { + /* Eg, "OPTIONS * HTTP/1.1" */ + url = g_strdup_printf ("%s://%s", + soup_socket_is_ssl (sock) ? "https" : "http", + req_host); + uri = soup_uri_new (url); + if (uri) + soup_uri_set_path (uri, "*"); + g_free (url); + } else if (msg->method == SOUP_METHOD_CONNECT) { + /* Authority */ + uri = parse_connect_authority (req_path); + } else if (*req_path != '/') { + /* Absolute URI */ + uri = soup_uri_new (req_path); + } else if (req_host) { + url = g_strdup_printf ("%s://%s%s", + soup_socket_is_ssl (sock) ? "https" : "http", + req_host, req_path); + uri = soup_uri_new (url); + g_free (url); + } else if (soup_message_get_http_version (msg) == SOUP_HTTP_1_0) { + /* No Host header, no AbsoluteUri */ + GInetSocketAddress *addr = soup_socket_get_local_address (sock); + GInetAddress *inet_addr = g_inet_socket_address_get_address (addr); + char *local_ip = g_inet_address_to_string (inet_addr); + + uri = soup_uri_new (NULL); + soup_uri_set_scheme (uri, soup_socket_is_ssl (sock) ? + SOUP_URI_SCHEME_HTTPS : + SOUP_URI_SCHEME_HTTP); + soup_uri_set_host (uri, local_ip); + soup_uri_set_port (uri, g_inet_socket_address_get_port (addr)); + soup_uri_set_path (uri, req_path); + g_free (local_ip); + } else + uri = NULL; + + g_free (req_path); + + if (!uri || !uri->host) { + if (uri) + soup_uri_free (uri); + return SOUP_STATUS_BAD_REQUEST; + } + + soup_message_set_uri (msg, uri); + soup_uri_free (uri); + + return SOUP_STATUS_OK; +} + +static void +handle_partial_get (SoupMessage *msg) +{ + SoupRange *ranges; + int nranges; + GBytes *full_response; + guint status; + + /* Make sure the message is set up right for us to return a + * partial response; it has to be a GET, the status must be + * 200 OK (and in particular, NOT already 206 Partial + * Content), and the SoupServer must have already filled in + * the response body + */ + if (msg->method != SOUP_METHOD_GET || + msg->status_code != SOUP_STATUS_OK || + soup_message_headers_get_encoding (msg->response_headers) != + SOUP_ENCODING_CONTENT_LENGTH || + msg->response_body->length == 0 || + !soup_message_body_get_accumulate (msg->response_body)) + return; + + /* Oh, and there has to have been a valid Range header on the + * request, of course. + */ + status = soup_message_headers_get_ranges_internal (msg->request_headers, + msg->response_body->length, + TRUE, + &ranges, &nranges); + if (status == SOUP_STATUS_REQUESTED_RANGE_NOT_SATISFIABLE) { + soup_message_set_status (msg, status); + soup_message_body_truncate (msg->response_body); + return; + } else if (status != SOUP_STATUS_PARTIAL_CONTENT) + return; + + full_response = soup_message_body_flatten (msg->response_body); + if (!full_response) { + soup_message_headers_free_ranges (msg->request_headers, ranges); + return; + } + + soup_message_set_status (msg, SOUP_STATUS_PARTIAL_CONTENT); + soup_message_body_truncate (msg->response_body); + + if (nranges == 1) { + GBytes *range_buf; + + /* Single range, so just set Content-Range and fix the body. */ + + soup_message_headers_set_content_range (msg->response_headers, + ranges[0].start, + ranges[0].end, + g_bytes_get_size (full_response)); + range_buf = g_bytes_new_from_bytes (full_response, + ranges[0].start, + ranges[0].end - ranges[0].start + 1); + soup_message_body_append_bytes (msg->response_body, range_buf); + g_bytes_unref (range_buf); + } else { + SoupMultipart *multipart; + SoupMessageHeaders *part_headers; + GBytes *part_body; + GBytes *body = NULL; + const char *content_type; + int i; + + /* Multiple ranges, so build a multipart/byteranges response + * to replace msg->response_body with. + */ + + multipart = soup_multipart_new ("multipart/byteranges"); + content_type = soup_message_headers_get_one (msg->response_headers, + "Content-Type"); + for (i = 0; i < nranges; i++) { + part_headers = soup_message_headers_new (SOUP_MESSAGE_HEADERS_MULTIPART); + if (content_type) { + soup_message_headers_append (part_headers, + "Content-Type", + content_type); + } + soup_message_headers_set_content_range (part_headers, + ranges[i].start, + ranges[i].end, + g_bytes_get_size (full_response)); + part_body = g_bytes_new_from_bytes (full_response, + ranges[i].start, + ranges[i].end - ranges[i].start + 1); + soup_multipart_append_part (multipart, part_headers, + part_body); + soup_message_headers_free (part_headers); + g_bytes_unref (part_body); + } + + soup_multipart_to_message (multipart, msg->response_headers, &body); + soup_message_body_append_bytes (msg->response_body, body); + g_bytes_unref (body); + soup_multipart_free (multipart); + } + + g_bytes_unref (full_response); + soup_message_headers_free_ranges (msg->request_headers, ranges); +} + +static void +get_response_headers (SoupMessage *msg, GString *headers, + SoupEncoding *encoding, gpointer user_data) +{ + SoupEncoding claimed_encoding; + SoupMessageHeadersIter iter; + const char *name, *value; + + if (msg->status_code == 0) + soup_message_set_status (msg, SOUP_STATUS_INTERNAL_SERVER_ERROR); + + handle_partial_get (msg); + + g_string_append_printf (headers, "HTTP/1.%c %d %s\r\n", + soup_message_get_http_version (msg) == SOUP_HTTP_1_0 ? '0' : '1', + msg->status_code, msg->reason_phrase); + + claimed_encoding = soup_message_headers_get_encoding (msg->response_headers); + if ((msg->method == SOUP_METHOD_HEAD || + msg->status_code == SOUP_STATUS_NO_CONTENT || + msg->status_code == SOUP_STATUS_NOT_MODIFIED || + SOUP_STATUS_IS_INFORMATIONAL (msg->status_code)) || + (msg->method == SOUP_METHOD_CONNECT && + SOUP_STATUS_IS_SUCCESSFUL (msg->status_code))) + *encoding = SOUP_ENCODING_NONE; + else + *encoding = claimed_encoding; + + if (claimed_encoding == SOUP_ENCODING_CONTENT_LENGTH && + !soup_message_headers_get_content_length (msg->response_headers)) { + soup_message_headers_set_content_length (msg->response_headers, + msg->response_body->length); + } + + soup_message_headers_iter_init (&iter, msg->response_headers); + while (soup_message_headers_iter_next (&iter, &name, &value)) + g_string_append_printf (headers, "%s: %s\r\n", name, value); + g_string_append (headers, "\r\n"); +} + +void +soup_message_read_request (SoupMessage *msg, + SoupSocket *sock, + SoupMessageCompletionFn completion_cb, + gpointer user_data) +{ + GMainContext *async_context; + GIOStream *iostream; + + async_context = g_main_context_ref_thread_default (); + iostream = soup_socket_get_iostream (sock); + + soup_message_io_server (msg, iostream, async_context, + get_response_headers, + parse_request_headers, + sock, + completion_cb, user_data); + g_main_context_unref (async_context); +} diff --git a/libsoup/server/soup-path-map.c b/libsoup/server/soup-path-map.c new file mode 100644 index 00000000..6be8eb67 --- /dev/null +++ b/libsoup/server/soup-path-map.c @@ -0,0 +1,192 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * soup-path-map.c: URI path prefix-matcher + * + * Copyright (C) 2007 Novell, Inc. + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <string.h> + +#include "soup-path-map.h" + +/* This could be replaced with something more clever, like a Patricia + * trie, but it's probably not worth it since the total number of + * mappings is likely to always be small. So we keep an array of + * paths, sorted by decreasing length. (The first prefix match will + * therefore be the longest.) + */ + +typedef struct { + char *path; + int len; + gpointer data; +} SoupPathMapping; + +struct SoupPathMap { + GArray *mappings; + GDestroyNotify free_func; +}; + +/** + * soup_path_map_new: + * @data_free_func: function to use to free data added with + * soup_path_map_add(). + * + * Creates a new %SoupPathMap. + * + * Return value: the new %SoupPathMap + **/ +SoupPathMap * +soup_path_map_new (GDestroyNotify data_free_func) +{ + SoupPathMap *map; + + map = g_slice_new0 (SoupPathMap); + map->mappings = g_array_new (FALSE, FALSE, sizeof (SoupPathMapping)); + map->free_func = data_free_func; + + return map; +} + +/** + * soup_path_map_free: + * @map: a %SoupPathMap + * + * Frees @map and all data stored in it. + **/ +void +soup_path_map_free (SoupPathMap *map) +{ + SoupPathMapping *mappings = (SoupPathMapping *)map->mappings->data; + guint i; + + for (i = 0; i < map->mappings->len; i++) { + g_free (mappings[i].path); + if (map->free_func) + map->free_func (mappings[i].data); + } + g_array_free (map->mappings, TRUE); + + g_slice_free (SoupPathMap, map); +} + +/* Scan @map looking for @path or one of its ancestors. + * Sets *@match to the index of a match, or -1 if no match is found. + * Sets *@insert to the index to insert @path at if a new mapping is + * desired. Returns %TRUE if *@match is an exact match. + */ +static gboolean +mapping_lookup (SoupPathMap *map, const char *path, int *match, int *insert) +{ + SoupPathMapping *mappings = (SoupPathMapping *)map->mappings->data; + guint i; + int path_len; + gboolean exact = FALSE; + + *match = -1; + + path_len = strcspn (path, "?"); + for (i = 0; i < map->mappings->len; i++) { + if (mappings[i].len > path_len) + continue; + + if (insert && mappings[i].len < path_len) { + *insert = i; + /* Clear insert so we don't try to set it again */ + insert = NULL; + } + + if (!strncmp (mappings[i].path, path, mappings[i].len)) { + *match = i; + if (path_len == mappings[i].len) + exact = TRUE; + if (!insert) + return exact; + } + } + + if (insert) + *insert = i; + return exact; +} + +/** + * soup_path_map_add: + * @map: a %SoupPathMap + * @path: the path + * @data: the data + * + * Adds @data to @map at @path. If there was already data at @path it + * will be freed. + **/ +void +soup_path_map_add (SoupPathMap *map, const char *path, gpointer data) +{ + SoupPathMapping *mappings = (SoupPathMapping *)map->mappings->data; + int match, insert; + + if (mapping_lookup (map, path, &match, &insert)) { + if (map->free_func) + map->free_func (mappings[match].data); + mappings[match].data = data; + } else { + SoupPathMapping mapping; + + mapping.path = g_strdup (path); + mapping.len = strlen (path); + mapping.data = data; + g_array_insert_val (map->mappings, insert, mapping); + } +} + +/** + * soup_path_map_remove: + * @map: a %SoupPathMap + * @path: the path + * + * Removes @data from @map at @path. (This must be called with the same + * path the data was originally added with, not a subdirectory.) + **/ +void +soup_path_map_remove (SoupPathMap *map, const char *path) +{ + SoupPathMapping *mappings = (SoupPathMapping *)map->mappings->data; + int match; + + if (!mapping_lookup (map, path, &match, NULL)) + return; + + if (map->free_func) + map->free_func (mappings[match].data); + g_free (mappings[match].path); + g_array_remove_index (map->mappings, match); +} + +/** + * soup_path_map_lookup: + * @map: a %SoupPathMap + * @path: the path + * + * Finds the data associated with @path in @map. If there is no data + * specifically associated with @path, it will return the data for the + * closest parent directory of @path that has data associated with it. + * + * Return value: (nullable): the data set with soup_path_map_add(), or + * %NULL if no data could be found for @path or any of its ancestors. + **/ +gpointer +soup_path_map_lookup (SoupPathMap *map, const char *path) +{ + SoupPathMapping *mappings = (SoupPathMapping *)map->mappings->data; + int match; + + mapping_lookup (map, path, &match, NULL); + if (match == -1) + return NULL; + else + return mappings[match].data; +} diff --git a/libsoup/server/soup-path-map.h b/libsoup/server/soup-path-map.h new file mode 100644 index 00000000..8c3b3367 --- /dev/null +++ b/libsoup/server/soup-path-map.h @@ -0,0 +1,26 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2007 Novell, Inc. + */ + +#ifndef __SOUP_PATH_MAP_H__ +#define __SOUP_PATH_MAP_H__ 1 + +#include "soup-types.h" + +typedef struct SoupPathMap SoupPathMap; + +SoupPathMap *soup_path_map_new (GDestroyNotify data_free_func); +void soup_path_map_free (SoupPathMap *map); + +void soup_path_map_add (SoupPathMap *map, + const char *path, + gpointer data); +void soup_path_map_remove (SoupPathMap *map, + const char *path); + +gpointer soup_path_map_lookup (SoupPathMap *map, + const char *path); + + +#endif /* __SOUP_PATH_MAP_H__ */ diff --git a/libsoup/server/soup-server-private.h b/libsoup/server/soup-server-private.h new file mode 100644 index 00000000..15430e73 --- /dev/null +++ b/libsoup/server/soup-server-private.h @@ -0,0 +1,23 @@ +/* + * Copyright 2020 Igalia S.L. + * + * This file 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 of the + * License, or (at your option) any later version. + * + * This file 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 program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +#pragma once + +#include "soup-server.h" + +SoupSocket *soup_client_context_get_soup_socket (SoupClientContext *client); diff --git a/libsoup/server/soup-server.c b/libsoup/server/soup-server.c new file mode 100644 index 00000000..aa5f2203 --- /dev/null +++ b/libsoup/server/soup-server.c @@ -0,0 +1,2364 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * soup-server.c: Asynchronous HTTP server + * + * Copyright (C) 2001-2003, Ximian, Inc. + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <string.h> + +#include <glib/gi18n-lib.h> + +#include "soup-server-private.h" +#include "soup.h" +#include "soup-message-private.h" +#include "soup-misc.h" +#include "soup-path-map.h" +#include "soup-socket-private.h" +#include "websocket/soup-websocket.h" +#include "websocket/soup-websocket-connection.h" +#include "websocket/soup-websocket-extension-deflate.h" + +/** + * SECTION:soup-server + * @short_description: HTTP server + * @see_also: #SoupAuthDomain + * + * #SoupServer implements a simple HTTP server. + * + * (The following documentation describes the current #SoupServer API, + * available in <application>libsoup</application> 2.48 and later. See + * the section "<link linkend="soup-server-old-api">The Old SoupServer + * Listening API</link>" in the server how-to documentation for + * details on the older #SoupServer API.) + * + * To begin, create a server using soup_server_new(). Add at least one + * handler by calling soup_server_add_handler() or + * soup_server_add_early_handler(); the handler will be called to + * process any requests underneath the path you pass. (If you want all + * requests to go to the same handler, just pass "/" (or %NULL) for + * the path.) + * + * When a new connection is accepted (or a new request is started on + * an existing persistent connection), the #SoupServer will emit + * #SoupServer::request-started and then begin processing the request + * as described below, but note that once the message is assigned a + * #SoupMessage:status-code, then callbacks after that point will be + * skipped. Note also that it is not defined when the callbacks happen + * relative to various #SoupMessage signals. + * + * Once the headers have been read, #SoupServer will check if there is + * a #SoupAuthDomain (qv) covering the Request-URI; if so, and if the + * message does not contain suitable authorization, then the + * #SoupAuthDomain will set a status of %SOUP_STATUS_UNAUTHORIZED on + * the message. + * + * After checking for authorization, #SoupServer will look for "early" + * handlers (added with soup_server_add_early_handler()) matching the + * Request-URI. If one is found, it will be run; in particular, this + * can be used to connect to signals to do a streaming read of the + * request body. + * + * (At this point, if the request headers contain "<literal>Expect: + * 100-continue</literal>", and a status code has been set, then + * #SoupServer will skip the remaining steps and return the response. + * If the request headers contain "<literal>Expect: + * 100-continue</literal>" and no status code has been set, + * #SoupServer will return a %SOUP_STATUS_CONTINUE status before + * continuing.) + * + * The server will then read in the response body (if present). At + * this point, if there are no handlers at all defined for the + * Request-URI, then the server will return %SOUP_STATUS_NOT_FOUND to + * the client. + * + * Otherwise (assuming no previous step assigned a status to the + * message) any "normal" handlers (added with + * soup_server_add_handler()) for the message's Request-URI will be + * run. + * + * Then, if the path has a WebSocket handler registered (and has + * not yet been assigned a status), #SoupServer will attempt to + * validate the WebSocket handshake, filling in the response and + * setting a status of %SOUP_STATUS_SWITCHING_PROTOCOLS or + * %SOUP_STATUS_BAD_REQUEST accordingly. + * + * If the message still has no status code at this point (and has not + * been paused with soup_server_pause_message()), then it will be + * given a status of %SOUP_STATUS_INTERNAL_SERVER_ERROR (because at + * least one handler ran, but returned without assigning a status). + * + * Finally, the server will emit #SoupServer::request-finished (or + * #SoupServer::request-aborted if an I/O error occurred before + * handling was completed). + * + * If you want to handle the special "*" URI (eg, "OPTIONS *"), you + * must explicitly register a handler for "*"; the default handler + * will not be used for that case. + * + * If you want to process https connections in addition to (or instead + * of) http connections, you can either set the + * %SOUP_SERVER_TLS_CERTIFICATE property when creating the server, or + * else call soup_server_set_ssl_cert_file() after creating it. + * + * Once the server is set up, make one or more calls to + * soup_server_listen(), soup_server_listen_local(), or + * soup_server_listen_all() to tell it where to listen for + * connections. (All ports on a #SoupServer use the same handlers; if + * you need to handle some ports differently, such as returning + * different data for http and https, you'll need to create multiple + * #SoupServers, or else check the passed-in URI in the handler + * function.). + * + * #SoupServer will begin processing connections as soon as you return + * to (or start) the main loop for the current thread-default + * #GMainContext. + */ + +enum { + REQUEST_STARTED, + REQUEST_READ, + REQUEST_FINISHED, + REQUEST_ABORTED, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL] = { 0 }; + +struct _SoupClientContext { + SoupServer *server; + SoupSocket *sock; + GSocket *gsock; + SoupMessage *msg; + SoupAuthDomain *auth_domain; + char *auth_user; + + GSocketAddress *remote_addr; + char *remote_ip; + GSocketAddress *local_addr; + + int ref_count; +}; + +typedef struct { + char *path; + + SoupServerCallback early_callback; + GDestroyNotify early_destroy; + gpointer early_user_data; + + SoupServerCallback callback; + GDestroyNotify destroy; + gpointer user_data; + + char *websocket_origin; + char **websocket_protocols; + GList *websocket_extensions; + SoupServerWebsocketCallback websocket_callback; + GDestroyNotify websocket_destroy; + gpointer websocket_user_data; +} SoupServerHandler; + +typedef struct { + GSList *listeners; + GSList *clients; + + GTlsCertificate *tls_cert; + + char *server_header; + + GMainContext *async_context; + GMainLoop *loop; + + gboolean raw_paths; + SoupPathMap *handlers; + + GSList *auth_domains; + + char **http_aliases, **https_aliases; + + GPtrArray *websocket_extension_types; + + gboolean disposed; + +} SoupServerPrivate; + +#define SOUP_SERVER_SERVER_HEADER_BASE "libsoup/" PACKAGE_VERSION + +enum { + PROP_0, + + PROP_TLS_CERT_FILE, + PROP_TLS_KEY_FILE, + PROP_TLS_CERTIFICATE, + PROP_RAW_PATHS, + PROP_SERVER_HEADER, + PROP_HTTP_ALIASES, + PROP_HTTPS_ALIASES, + PROP_ADD_WEBSOCKET_EXTENSION, + PROP_REMOVE_WEBSOCKET_EXTENSION, + + LAST_PROP +}; + +G_DEFINE_TYPE_WITH_PRIVATE (SoupServer, soup_server, G_TYPE_OBJECT) + +static SoupClientContext *soup_client_context_ref (SoupClientContext *client); +static void soup_client_context_unref (SoupClientContext *client); + +static void +free_handler (SoupServerHandler *handler) +{ + g_free (handler->path); + g_free (handler->websocket_origin); + g_strfreev (handler->websocket_protocols); + g_list_free_full (handler->websocket_extensions, g_object_unref); + if (handler->early_destroy) + handler->early_destroy (handler->early_user_data); + if (handler->destroy) + handler->destroy (handler->user_data); + if (handler->websocket_destroy) + handler->websocket_destroy (handler->websocket_user_data); + g_slice_free (SoupServerHandler, handler); +} + +static void +soup_server_init (SoupServer *server) +{ + SoupServerPrivate *priv = soup_server_get_instance_private (server); + + priv->handlers = soup_path_map_new ((GDestroyNotify)free_handler); + + priv->http_aliases = g_new (char *, 2); + priv->http_aliases[0] = (char *)g_intern_string ("*"); + priv->http_aliases[1] = NULL; + + priv->websocket_extension_types = g_ptr_array_new_with_free_func ((GDestroyNotify)g_type_class_unref); + + /* Use permessage-deflate extension by default */ + g_ptr_array_add (priv->websocket_extension_types, g_type_class_ref (SOUP_TYPE_WEBSOCKET_EXTENSION_DEFLATE)); +} + +static void +soup_server_dispose (GObject *object) +{ + SoupServer *server = SOUP_SERVER (object); + SoupServerPrivate *priv = soup_server_get_instance_private (server); + + priv->disposed = TRUE; + soup_server_disconnect (server); + + G_OBJECT_CLASS (soup_server_parent_class)->dispose (object); +} + +static void +soup_server_finalize (GObject *object) +{ + SoupServer *server = SOUP_SERVER (object); + SoupServerPrivate *priv = soup_server_get_instance_private (server); + + g_clear_object (&priv->tls_cert); + + g_free (priv->server_header); + + soup_path_map_free (priv->handlers); + + g_slist_free_full (priv->auth_domains, g_object_unref); + + g_clear_pointer (&priv->loop, g_main_loop_unref); + + g_free (priv->http_aliases); + g_free (priv->https_aliases); + + g_ptr_array_free (priv->websocket_extension_types, TRUE); + + G_OBJECT_CLASS (soup_server_parent_class)->finalize (object); +} + +/* priv->http_aliases and priv->https_aliases are stored as arrays of + * *interned* strings, so we can't just use g_strdupv() to set them. + */ +static void +set_aliases (char ***variable, char **value) +{ + int len, i; + + if (*variable) + g_free (*variable); + + if (!value) { + *variable = NULL; + return; + } + + len = g_strv_length (value); + *variable = g_new (char *, len + 1); + for (i = 0; i < len; i++) + (*variable)[i] = (char *)g_intern_string (value[i]); + (*variable)[i] = NULL; +} + +static void +soup_server_set_property (GObject *object, guint prop_id, + const GValue *value, GParamSpec *pspec) +{ + SoupServer *server = SOUP_SERVER (object); + SoupServerPrivate *priv = soup_server_get_instance_private (server); + const char *header; + + switch (prop_id) { + case PROP_TLS_CERTIFICATE: + if (priv->tls_cert) + g_object_unref (priv->tls_cert); + priv->tls_cert = g_value_dup_object (value); + break; + case PROP_RAW_PATHS: + priv->raw_paths = g_value_get_boolean (value); + break; + case PROP_SERVER_HEADER: + g_free (priv->server_header); + header = g_value_get_string (value); + if (!header) + priv->server_header = NULL; + else if (!*header) { + priv->server_header = + g_strdup (SOUP_SERVER_SERVER_HEADER_BASE); + } else if (g_str_has_suffix (header, " ")) { + priv->server_header = + g_strdup_printf ("%s%s", header, + SOUP_SERVER_SERVER_HEADER_BASE); + } else + priv->server_header = g_strdup (header); + break; + case PROP_HTTP_ALIASES: + set_aliases (&priv->http_aliases, g_value_get_boxed (value)); + break; + case PROP_HTTPS_ALIASES: + set_aliases (&priv->https_aliases, g_value_get_boxed (value)); + break; + case PROP_ADD_WEBSOCKET_EXTENSION: + soup_server_add_websocket_extension (server, g_value_get_gtype (value)); + break; + case PROP_REMOVE_WEBSOCKET_EXTENSION: + soup_server_remove_websocket_extension (server, g_value_get_gtype (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +soup_server_get_property (GObject *object, guint prop_id, + GValue *value, GParamSpec *pspec) +{ + SoupServer *server = SOUP_SERVER (object); + SoupServerPrivate *priv = soup_server_get_instance_private (server); + + switch (prop_id) { + case PROP_TLS_CERTIFICATE: + g_value_set_object (value, priv->tls_cert); + break; + case PROP_RAW_PATHS: + g_value_set_boolean (value, priv->raw_paths); + break; + case PROP_SERVER_HEADER: + g_value_set_string (value, priv->server_header); + break; + case PROP_HTTP_ALIASES: + g_value_set_boxed (value, priv->http_aliases); + break; + case PROP_HTTPS_ALIASES: + g_value_set_boxed (value, priv->https_aliases); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +soup_server_class_init (SoupServerClass *server_class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (server_class); + + /* virtual method override */ + object_class->dispose = soup_server_dispose; + object_class->finalize = soup_server_finalize; + object_class->set_property = soup_server_set_property; + object_class->get_property = soup_server_get_property; + + /* signals */ + + /** + * SoupServer::request-started: + * @server: the server + * @message: the new message + * @client: the client context + * + * Emitted when the server has started reading a new request. + * @message will be completely blank; not even the + * Request-Line will have been read yet. About the only thing + * you can usefully do with it is connect to its signals. + * + * If the request is read successfully, this will eventually + * be followed by a #SoupServer::request_read signal. If a + * response is then sent, the request processing will end with + * a #SoupServer::request_finished signal. If a network error + * occurs, the processing will instead end with + * #SoupServer::request_aborted. + **/ + signals[REQUEST_STARTED] = + g_signal_new ("request-started", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_FIRST, + G_STRUCT_OFFSET (SoupServerClass, request_started), + NULL, NULL, + NULL, + G_TYPE_NONE, 2, + SOUP_TYPE_MESSAGE, + SOUP_TYPE_CLIENT_CONTEXT); + + /** + * SoupServer::request-read: + * @server: the server + * @message: the message + * @client: the client context + * + * Emitted when the server has successfully read a request. + * @message will have all of its request-side information + * filled in, and if the message was authenticated, @client + * will have information about that. This signal is emitted + * before any (non-early) handlers are called for the message, + * and if it sets the message's #status_code, then normal + * handler processing will be skipped. + **/ + signals[REQUEST_READ] = + g_signal_new ("request-read", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_FIRST, + G_STRUCT_OFFSET (SoupServerClass, request_read), + NULL, NULL, + NULL, + G_TYPE_NONE, 2, + SOUP_TYPE_MESSAGE, + SOUP_TYPE_CLIENT_CONTEXT); + + /** + * SoupServer::request-finished: + * @server: the server + * @message: the message + * @client: the client context + * + * Emitted when the server has finished writing a response to + * a request. + **/ + signals[REQUEST_FINISHED] = + g_signal_new ("request-finished", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_FIRST, + G_STRUCT_OFFSET (SoupServerClass, request_finished), + NULL, NULL, + NULL, + G_TYPE_NONE, 2, + SOUP_TYPE_MESSAGE, + SOUP_TYPE_CLIENT_CONTEXT); + + /** + * SoupServer::request-aborted: + * @server: the server + * @message: the message + * @client: the client context + * + * Emitted when processing has failed for a message; this + * could mean either that it could not be read (if + * #SoupServer::request_read has not been emitted for it yet), + * or that the response could not be written back (if + * #SoupServer::request_read has been emitted but + * #SoupServer::request_finished has not been). + * + * @message is in an undefined state when this signal is + * emitted; the signal exists primarily to allow the server to + * free any state that it may have allocated in + * #SoupServer::request_started. + **/ + signals[REQUEST_ABORTED] = + g_signal_new ("request-aborted", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_FIRST, + G_STRUCT_OFFSET (SoupServerClass, request_aborted), + NULL, NULL, + NULL, + G_TYPE_NONE, 2, + SOUP_TYPE_MESSAGE, + SOUP_TYPE_CLIENT_CONTEXT); + + /* properties */ + /** + * SOUP_SERVER_TLS_CERTIFICATE: + * + * Alias for the #SoupServer:tls-certificate property, qv. + * + * Since: 2.38 + */ + /** + * SoupServer:tls-certificate: + * + * A #GTlsCertificate that has a #GTlsCertificate:private-key + * set. If this is set, then the server will be able to speak + * https in addition to (or instead of) plain http. + * + * Alternatively, you can call soup_server_set_ssl_cert_file() + * to have #SoupServer read in a a certificate from a file. + * + * Since: 2.38 + */ + g_object_class_install_property ( + object_class, PROP_TLS_CERTIFICATE, + g_param_spec_object (SOUP_SERVER_TLS_CERTIFICATE, + "TLS certificate", + "GTlsCertificate to use for https", + G_TYPE_TLS_CERTIFICATE, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + /** + * SOUP_SERVER_RAW_PATHS: + * + * Alias for the #SoupServer:raw-paths property. (If %TRUE, + * percent-encoding in the Request-URI path will not be + * automatically decoded.) + **/ + g_object_class_install_property ( + object_class, PROP_RAW_PATHS, + g_param_spec_boolean (SOUP_SERVER_RAW_PATHS, + "Raw paths", + "If %TRUE, percent-encoding in the Request-URI path will not be automatically decoded.", + FALSE, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + /** + * SoupServer:server-header: + * + * If non-%NULL, the value to use for the "Server" header on + * #SoupMessage<!-- -->s processed by this server. + * + * The Server header is the server equivalent of the + * User-Agent header, and provides information about the + * server and its components. It contains a list of one or + * more product tokens, separated by whitespace, with the most + * significant product token coming first. The tokens must be + * brief, ASCII, and mostly alphanumeric (although "-", "_", + * and "." are also allowed), and may optionally include a "/" + * followed by a version string. You may also put comments, + * enclosed in parentheses, between or after the tokens. + * + * Some HTTP server implementations intentionally do not use + * version numbers in their Server header, so that + * installations running older versions of the server don't + * end up advertising their vulnerability to specific security + * holes. + * + * As with #SoupSession:user_agent, if you set a + * #SoupServer:server_header property that has trailing whitespace, + * #SoupServer will append its own product token (eg, + * "<literal>libsoup/2.3.2</literal>") to the end of the + * header for you. + **/ + /** + * SOUP_SERVER_SERVER_HEADER: + * + * Alias for the #SoupServer:server-header property, qv. + **/ + g_object_class_install_property ( + object_class, PROP_SERVER_HEADER, + g_param_spec_string (SOUP_SERVER_SERVER_HEADER, + "Server header", + "Server header", + NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS)); + + /** + * SoupServer:http-aliases: + * + * A %NULL-terminated array of URI schemes that should be + * considered to be aliases for "http". Eg, if this included + * <literal>"dav"</literal>, than a URI of + * <literal>dav://example.com/path</literal> would be treated + * identically to <literal>http://example.com/path</literal>. + * In particular, this is needed in cases where a client + * sends requests with absolute URIs, where those URIs do + * not use "http:". + * + * The default value is an array containing the single element + * <literal>"*"</literal>, a special value which means that + * any scheme except "https" is considered to be an alias for + * "http". + * + * See also #SoupServer:https-aliases. + * + * Since: 2.44 + */ + /** + * SOUP_SERVER_HTTP_ALIASES: + * + * Alias for the #SoupServer:http-aliases property, qv. + * + * Since: 2.44 + */ + g_object_class_install_property ( + object_class, PROP_HTTP_ALIASES, + g_param_spec_boxed (SOUP_SERVER_HTTP_ALIASES, + "http aliases", + "URI schemes that are considered aliases for 'http'", + G_TYPE_STRV, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + /** + * SoupServer:https-aliases: + * + * A comma-delimited list of URI schemes that should be + * considered to be aliases for "https". See + * #SoupServer:http-aliases for more information. + * + * The default value is %NULL, meaning that no URI schemes + * are considered aliases for "https". + * + * Since: 2.44 + */ + /** + * SOUP_SERVER_HTTPS_ALIASES: + * + * Alias for the #SoupServer:https-aliases property, qv. + * + * Since: 2.44 + **/ + g_object_class_install_property ( + object_class, PROP_HTTPS_ALIASES, + g_param_spec_boxed (SOUP_SERVER_HTTPS_ALIASES, + "https aliases", + "URI schemes that are considered aliases for 'https'", + G_TYPE_STRV, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + /** + * SoupServer:add-websocket-extension: (skip) + * + * Add support for #SoupWebsocketExtension of the given type. + * (Shortcut for calling soup_server_add_websocket_extension().) + * + * Since: 2.68 + **/ + /** + * SOUP_SERVER_ADD_WEBSOCKET_EXTENSION: (skip) + * + * Alias for the #SoupServer:add-websocket-extension property, qv. + * + * Since: 2.68 + **/ + g_object_class_install_property ( + object_class, PROP_ADD_WEBSOCKET_EXTENSION, + g_param_spec_gtype (SOUP_SERVER_ADD_WEBSOCKET_EXTENSION, + "Add support for a WebSocket extension", + "Add support for a WebSocket extension of the given type", + SOUP_TYPE_WEBSOCKET_EXTENSION, + G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS)); + /** + * SoupServer:remove-websocket-extension: (skip) + * + * Remove support for #SoupWebsocketExtension of the given type. (Shortcut for + * calling soup_server_remove_websocket_extension().) + * + * Since: 2.68 + **/ + /** + * SOUP_SERVER_REMOVE_WEBSOCKET_EXTENSION: (skip) + * + * Alias for the #SoupServer:remove-websocket-extension property, qv. + * + * Since: 2.68 + **/ + g_object_class_install_property ( + object_class, PROP_REMOVE_WEBSOCKET_EXTENSION, + g_param_spec_gtype (SOUP_SERVER_REMOVE_WEBSOCKET_EXTENSION, + "Remove support for a WebSocket extension", + "Remove support for a WebSocket extension of the given type", + SOUP_TYPE_WEBSOCKET_EXTENSION, + G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS)); +} + +/** + * soup_server_new: + * @optname1: name of first property to set + * @...: value of @optname1, followed by additional property/value pairs + * + * Creates a new #SoupServer. This is exactly equivalent to calling + * g_object_new() and specifying %SOUP_TYPE_SERVER as the type. + * + * Return value: (nullable): a new #SoupServer. If you are using + * certain legacy properties, this may also return %NULL if an error + * occurs. + **/ +SoupServer * +soup_server_new (const char *optname1, ...) +{ + SoupServer *server; + va_list ap; + + va_start (ap, optname1); + server = (SoupServer *)g_object_new_valist (SOUP_TYPE_SERVER, + optname1, ap); + va_end (ap); + + return server; +} + +/** + * soup_server_set_ssl_cert_file: + * @server: a #SoupServer + * @ssl_cert_file: path to a file containing a PEM-encoded SSL/TLS + * certificate. + * @ssl_key_file: path to a file containing a PEM-encoded private key. + * @error: return location for a #GError + * + * Sets @server up to do https, using the SSL/TLS certificate + * specified by @ssl_cert_file and @ssl_key_file (which may point to + * the same file). + * + * Alternatively, you can set the #SoupServer:tls-certificate property + * at construction time, if you already have a #GTlsCertificate. + * + * Return value: success or failure. + * + * Since: 2.48 + */ +gboolean +soup_server_set_ssl_cert_file (SoupServer *server, + const char *ssl_cert_file, + const char *ssl_key_file, + GError **error) +{ + SoupServerPrivate *priv; + + g_return_val_if_fail (SOUP_IS_SERVER (server), FALSE); + priv = soup_server_get_instance_private (server); + + if (priv->tls_cert) + g_object_unref (priv->tls_cert); + + priv->tls_cert = g_tls_certificate_new_from_files (ssl_cert_file, + ssl_key_file, + error); + return priv->tls_cert != NULL; +} + +/** + * soup_server_is_https: + * @server: a #SoupServer + * + * Checks whether @server is capable of https. + * + * In order for a server to run https, you must call + * soup_server_set_ssl_cert_file(), or set the + * #SoupServer:tls-certificate property, to provide it with a + * certificate to use. + * + * If you are using the deprecated single-listener APIs, then a return + * value of %TRUE indicates that the #SoupServer serves https + * exclusively. If you are using soup_server_listen(), etc, then a + * %TRUE return value merely indicates that the server is + * <emphasis>able</emphasis> to do https, regardless of whether it + * actually currently is or not. Use soup_server_get_uris() to see if + * it currently has any https listeners. + * + * Return value: %TRUE if @server is configured to serve https. + **/ +gboolean +soup_server_is_https (SoupServer *server) +{ + SoupServerPrivate *priv; + + g_return_val_if_fail (SOUP_IS_SERVER (server), 0); + priv = soup_server_get_instance_private (server); + + return priv->tls_cert != NULL; +} + +/** + * soup_server_get_listeners: + * @server: a #SoupServer + * + * Gets @server's list of listening sockets. + * + * You should treat these sockets as read-only; writing to or + * modifiying any of these sockets may cause @server to malfunction. + * + * Return value: (transfer container) (element-type Gio.Socket): a + * list of listening sockets. + **/ +GSList * +soup_server_get_listeners (SoupServer *server) +{ + SoupServerPrivate *priv; + GSList *listeners, *iter; + + g_return_val_if_fail (SOUP_IS_SERVER (server), NULL); + priv = soup_server_get_instance_private (server); + + listeners = NULL; + for (iter = priv->listeners; iter; iter = iter->next) + listeners = g_slist_prepend (listeners, soup_socket_get_gsocket (iter->data)); + + /* priv->listeners has the sockets in reverse order from how + * they were added, so listeners now has them back in the + * original order. + */ + return listeners; +} + +static void start_request (SoupServer *, SoupClientContext *); +static void socket_disconnected (SoupSocket *sock, SoupClientContext *client); + +static SoupClientContext * +soup_client_context_new (SoupServer *server, SoupSocket *sock) +{ + SoupClientContext *client = g_slice_new0 (SoupClientContext); + + client->server = server; + client->sock = g_object_ref (sock); + client->gsock = soup_socket_get_gsocket (sock); + if (client->gsock) + g_object_ref (client->gsock); + g_signal_connect (sock, "disconnected", + G_CALLBACK (socket_disconnected), client); + client->ref_count = 1; + + return client; +} + +static void +soup_client_context_cleanup (SoupClientContext *client) +{ + g_clear_object (&client->auth_domain); + g_clear_pointer (&client->auth_user, g_free); + g_clear_object (&client->remote_addr); + g_clear_object (&client->local_addr); + + client->msg = NULL; +} + +static SoupClientContext * +soup_client_context_ref (SoupClientContext *client) +{ + g_atomic_int_inc (&client->ref_count); + return client; +} + +static void +soup_client_context_unref (SoupClientContext *client) +{ + if (!g_atomic_int_dec_and_test (&client->ref_count)) + return; + + soup_client_context_cleanup (client); + + g_signal_handlers_disconnect_by_func (client->sock, socket_disconnected, client); + g_object_unref (client->sock); + g_clear_object (&client->gsock); + g_clear_pointer (&client->remote_ip, g_free); + g_slice_free (SoupClientContext, client); +} + +static void +request_finished (SoupMessage *msg, SoupMessageIOCompletion completion, gpointer user_data) +{ + SoupClientContext *client = user_data; + SoupServer *server = client->server; + SoupServerPrivate *priv = soup_server_get_instance_private (server); + SoupSocket *sock = client->sock; + gboolean failed; + + if (completion == SOUP_MESSAGE_IO_STOLEN) { + soup_client_context_unref (client); + g_object_unref (msg); + return; + } + + /* Complete the message, assuming it actually really started. */ + if (msg->method) { + soup_message_finished (msg); + + failed = (completion == SOUP_MESSAGE_IO_INTERRUPTED || + msg->status_code == SOUP_STATUS_IO_ERROR); + g_signal_emit (server, + failed ? signals[REQUEST_ABORTED] : signals[REQUEST_FINISHED], + 0, msg, client); + } + + if (completion == SOUP_MESSAGE_IO_COMPLETE && + soup_socket_is_connected (sock) && + soup_message_is_keepalive (msg) && + priv->listeners) { + start_request (server, client); + } else { + soup_socket_disconnect (client->sock); + soup_client_context_unref (client); + } + g_object_unref (msg); +} + +/* "" was never documented as meaning the same thing as "/", but it + * effectively was. We have to special case it now or otherwise it + * would match "*" too. + */ +#define NORMALIZED_PATH(path) ((path) && *(path) ? (path) : "/") + +static SoupServerHandler * +get_handler (SoupServer *server, SoupMessage *msg) +{ + SoupServerPrivate *priv = soup_server_get_instance_private (server); + SoupURI *uri; + + uri = soup_message_get_uri (msg); + return soup_path_map_lookup (priv->handlers, NORMALIZED_PATH (uri->path)); +} + +static void +call_handler (SoupServer *server, SoupServerHandler *handler, + SoupClientContext *client, SoupMessage *msg, + gboolean early) +{ + GHashTable *form_data_set; + SoupURI *uri; + + if (early && !handler->early_callback) + return; + else if (!early && !handler->callback) + return; + + if (msg->status_code != 0) + return; + + uri = soup_message_get_uri (msg); + if (uri->query) + form_data_set = soup_form_decode (uri->query); + else + form_data_set = NULL; + + if (early) { + (*handler->early_callback) (server, msg, + uri->path, form_data_set, + client, handler->early_user_data); + } else { + (*handler->callback) (server, msg, + uri->path, form_data_set, + client, handler->user_data); + } + + if (form_data_set) + g_hash_table_unref (form_data_set); +} + +static void +got_headers (SoupMessage *msg, SoupClientContext *client) +{ + SoupServer *server = client->server; + SoupServerPrivate *priv = soup_server_get_instance_private (server); + SoupServerHandler *handler; + SoupURI *uri; + GDateTime *date; + char *date_string; + SoupAuthDomain *domain; + GSList *iter; + gboolean rejected = FALSE; + char *auth_user; + + /* Add required response headers */ + date = g_date_time_new_now_utc (); + date_string = soup_date_time_to_string (date, SOUP_DATE_HTTP); + soup_message_headers_replace (msg->response_headers, "Date", + date_string); + g_free (date_string); + g_date_time_unref (date); + + if (msg->status_code != 0) + return; + + uri = soup_message_get_uri (msg); + if ((soup_socket_is_ssl (client->sock) && !soup_uri_is_https (uri, priv->https_aliases)) || + (!soup_socket_is_ssl (client->sock) && !soup_uri_is_http (uri, priv->http_aliases))) { + soup_message_set_status (msg, SOUP_STATUS_BAD_REQUEST); + return; + } + + if (!priv->raw_paths) { + char *decoded_path; + + decoded_path = soup_uri_decode (uri->path); + + if (strstr (decoded_path, "/../") || + g_str_has_suffix (decoded_path, "/..") +#ifdef G_OS_WIN32 + || + strstr (decoded_path, "\\..\\") || + strstr (decoded_path, "/..\\") || + strstr (decoded_path, "\\../") || + g_str_has_suffix (decoded_path, "\\..") +#endif + ) { + /* Introducing new ".." segments is not allowed */ + g_free (decoded_path); + soup_message_set_status (msg, SOUP_STATUS_BAD_REQUEST); + return; + } + + soup_uri_set_path (uri, decoded_path); + g_free (decoded_path); + } + + /* Now handle authentication. (We do this here so that if + * the request uses "Expect: 100-continue", we can reject it + * immediately rather than waiting for the request body to + * be sent. + */ + for (iter = priv->auth_domains; iter; iter = iter->next) { + domain = iter->data; + + if (soup_auth_domain_covers (domain, msg)) { + auth_user = soup_auth_domain_accepts (domain, msg); + if (auth_user) { + client->auth_domain = g_object_ref (domain); + client->auth_user = auth_user; + return; + } + + rejected = TRUE; + } + } + + /* If any auth domain rejected it, then it will need authentication. */ + if (rejected) { + for (iter = priv->auth_domains; iter; iter = iter->next) { + domain = iter->data; + + if (soup_auth_domain_covers (domain, msg)) + soup_auth_domain_challenge (domain, msg); + } + return; + } + + /* Otherwise, call the early handlers. */ + handler = get_handler (server, msg); + if (handler) + call_handler (server, handler, client, msg, TRUE); +} + +static void +complete_websocket_upgrade (SoupMessage *msg, gpointer user_data) +{ + SoupClientContext *client = user_data; + SoupServer *server = client->server; + SoupURI *uri = soup_message_get_uri (msg); + SoupServerHandler *handler; + GIOStream *stream; + SoupWebsocketConnection *conn; + + handler = get_handler (server, msg); + if (!handler || !handler->websocket_callback) + return; + + soup_client_context_ref (client); + stream = soup_client_context_steal_connection (client); + conn = soup_websocket_connection_new_with_extensions (stream, uri, + SOUP_WEBSOCKET_CONNECTION_SERVER, + soup_message_headers_get_one (msg->request_headers, "Origin"), + soup_message_headers_get_one (msg->response_headers, "Sec-WebSocket-Protocol"), + handler->websocket_extensions); + handler->websocket_extensions = NULL; + g_object_unref (stream); + soup_client_context_unref (client); + + (*handler->websocket_callback) (server, conn, uri->path, client, + handler->websocket_user_data); + g_object_unref (conn); + soup_client_context_unref (client); +} + +static void +got_body (SoupMessage *msg, SoupClientContext *client) +{ + SoupServer *server = client->server; + SoupServerHandler *handler; + + g_signal_emit (server, signals[REQUEST_READ], 0, msg, client); + + if (msg->status_code != 0) + return; + + handler = get_handler (server, msg); + if (!handler) { + soup_message_set_status (msg, SOUP_STATUS_NOT_FOUND); + return; + } + + call_handler (server, handler, client, msg, FALSE); + if (msg->status_code != 0) + return; + + if (handler->websocket_callback) { + SoupServerPrivate *priv; + + priv = soup_server_get_instance_private (server); + if (soup_websocket_server_process_handshake_with_extensions (msg, + handler->websocket_origin, + handler->websocket_protocols, + priv->websocket_extension_types, + &handler->websocket_extensions)) { + g_signal_connect (msg, "wrote-informational", + G_CALLBACK (complete_websocket_upgrade), + soup_client_context_ref (client)); + } + } +} + +static void +start_request (SoupServer *server, SoupClientContext *client) +{ + SoupServerPrivate *priv = soup_server_get_instance_private (server); + SoupMessage *msg; + + soup_client_context_cleanup (client); + + /* Listen for another request on this connection */ + msg = g_object_new (SOUP_TYPE_MESSAGE, + SOUP_MESSAGE_SERVER_SIDE, TRUE, + NULL); + client->msg = msg; + + if (priv->server_header) { + soup_message_headers_append (msg->response_headers, "Server", + priv->server_header); + } + + g_signal_connect (msg, "got_headers", G_CALLBACK (got_headers), client); + g_signal_connect (msg, "got_body", G_CALLBACK (got_body), client); + + g_signal_emit (server, signals[REQUEST_STARTED], 0, + msg, client); + + soup_message_read_request (msg, client->sock, + request_finished, client); +} + +static void +socket_disconnected (SoupSocket *sock, SoupClientContext *client) +{ + SoupServerPrivate *priv = soup_server_get_instance_private (client->server); + + priv->clients = g_slist_remove (priv->clients, client); + + if (client->msg) { + soup_message_set_status (client->msg, SOUP_STATUS_IO_ERROR); + soup_message_io_finished (client->msg); + } +} + +static void +soup_server_accept_socket (SoupServer *server, + SoupSocket *sock) +{ + SoupServerPrivate *priv = soup_server_get_instance_private (server); + SoupClientContext *client; + + client = soup_client_context_new (server, sock); + priv->clients = g_slist_prepend (priv->clients, client); + start_request (server, client); +} + +/** + * soup_server_accept_iostream: + * @server: a #SoupServer + * @stream: a #GIOStream + * @local_addr: (allow-none): the local #GSocketAddress associated with the @stream + * @remote_addr: (allow-none): the remote #GSocketAddress associated with the @stream + * @error: return location for a #GError + * + * Add a new client stream to the @server. + * + * Return value: %TRUE on success, %FALSE if the stream could not be + * accepted or any other error occurred (in which case @error will be + * set). + * + * Since: 2.50 + **/ +gboolean +soup_server_accept_iostream (SoupServer *server, + GIOStream *stream, + GSocketAddress *local_addr, + GSocketAddress *remote_addr, + GError **error) +{ + SoupSocket *sock; + + sock = g_initable_new (SOUP_TYPE_SOCKET, NULL, error, + "iostream", stream, + SOUP_SOCKET_LOCAL_ADDRESS, local_addr, + SOUP_SOCKET_REMOTE_CONNECTABLE, remote_addr, + NULL); + + if (!sock) + return FALSE; + + soup_server_accept_socket (server, sock); + g_object_unref (sock); + + return TRUE; +} + +static void +new_connection (SoupSocket *listener, SoupSocket *sock, gpointer user_data) +{ + SoupServer *server = user_data; + + soup_server_accept_socket (server, sock); +} + +/** + * soup_server_disconnect: + * @server: a #SoupServer + * + * Closes and frees @server's listening sockets. + * + * Note that if there are currently requests in progress on @server, + * that they will continue to be processed if @server's #GMainContext + * is still running. + * + * You can call soup_server_listen(), etc, after calling this function + * if you want to start listening again. + **/ +void +soup_server_disconnect (SoupServer *server) +{ + SoupServerPrivate *priv; + GSList *listeners, *clients, *iter; + SoupSocket *listener; + SoupClientContext *client; + + g_return_if_fail (SOUP_IS_SERVER (server)); + priv = soup_server_get_instance_private (server); + + clients = priv->clients; + priv->clients = NULL; + listeners = priv->listeners; + priv->listeners = NULL; + + for (iter = clients; iter; iter = iter->next) { + client = iter->data; + soup_socket_disconnect (client->sock); + } + g_slist_free (clients); + + for (iter = listeners; iter; iter = iter->next) { + listener = iter->data; + soup_socket_disconnect (listener); + g_object_unref (listener); + } + g_slist_free (listeners); +} + +/** + * SoupServerListenOptions: + * @SOUP_SERVER_LISTEN_HTTPS: Listen for https connections rather + * than plain http. + * @SOUP_SERVER_LISTEN_IPV4_ONLY: Only listen on IPv4 interfaces. + * @SOUP_SERVER_LISTEN_IPV6_ONLY: Only listen on IPv6 interfaces. + * + * Options to pass to soup_server_listen(), etc. + * + * %SOUP_SERVER_LISTEN_IPV4_ONLY and %SOUP_SERVER_LISTEN_IPV6_ONLY + * only make sense with soup_server_listen_all() and + * soup_server_listen_local(), not plain soup_server_listen() (which + * simply listens on whatever kind of socket you give it). And you + * cannot specify both of them in a single call. + * + * Since: 2.48 + */ + +static gboolean +soup_server_listen_internal (SoupServer *server, SoupSocket *listener, + SoupServerListenOptions options, + GError **error) +{ + SoupServerPrivate *priv = soup_server_get_instance_private (server); + gboolean is_listening; + + if (options & SOUP_SERVER_LISTEN_HTTPS) { + if (!priv->tls_cert) { + g_set_error_literal (error, + G_IO_ERROR, + G_IO_ERROR_INVALID_ARGUMENT, + _("Can’t create a TLS server without a TLS certificate")); + return FALSE; + } + + g_object_set (G_OBJECT (listener), + SOUP_SOCKET_SSL_CREDENTIALS, priv->tls_cert, + NULL); + } + + g_object_get (G_OBJECT (listener), + SOUP_SOCKET_IS_SERVER, &is_listening, + NULL); + if (!is_listening) { + if (!soup_socket_listen_full (listener, error)) { + GInetSocketAddress *addr = soup_socket_get_local_address (listener); + GInetAddress *inet_addr = g_inet_socket_address_get_address (addr); + char *local_ip = g_inet_address_to_string (inet_addr); + + g_prefix_error (error, + _("Could not listen on address %s, port %d: "), + local_ip, + g_inet_socket_address_get_port (addr)); + g_free (local_ip); + return FALSE; + } + } + + g_signal_connect (listener, "new_connection", + G_CALLBACK (new_connection), server); + + /* Note: soup_server_listen_ipv4_ipv6() below relies on the + * fact that this does g_slist_prepend(). + */ + priv->listeners = g_slist_prepend (priv->listeners, g_object_ref (listener)); + return TRUE; +} + +/** + * soup_server_listen: + * @server: a #SoupServer + * @address: the address of the interface to listen on + * @options: listening options for this server + * @error: return location for a #GError + * + * This attempts to set up @server to listen for connections on + * @address. + * + * If @options includes %SOUP_SERVER_LISTEN_HTTPS, and @server has + * been configured for TLS, then @server will listen for https + * connections on this port. Otherwise it will listen for plain http. + * + * You may call this method (along with the other "listen" methods) + * any number of times on a server, if you want to listen on multiple + * ports, or set up both http and https service. + * + * After calling this method, @server will begin accepting and + * processing connections as soon as the appropriate #GMainContext is + * run. + * + * Note that #SoupServer never makes use of dual IPv4/IPv6 sockets; if + * @address is an IPv6 address, it will only accept IPv6 connections. + * You must configure IPv4 listening separately. + * + * Return value: %TRUE on success, %FALSE if @address could not be + * bound or any other error occurred (in which case @error will be + * set). + * + * Since: 2.48 + **/ +gboolean +soup_server_listen (SoupServer *server, GSocketAddress *address, + SoupServerListenOptions options, + GError **error) +{ + SoupServerPrivate *priv; + SoupSocket *listener; + gboolean success, ipv6_only; + + g_return_val_if_fail (SOUP_IS_SERVER (server), FALSE); + g_return_val_if_fail (!(options & SOUP_SERVER_LISTEN_IPV4_ONLY) && + !(options & SOUP_SERVER_LISTEN_IPV6_ONLY), FALSE); + + priv = soup_server_get_instance_private (server); + g_return_val_if_fail (priv->disposed == FALSE, FALSE); + + ipv6_only = g_socket_address_get_family (address) == G_SOCKET_FAMILY_IPV6; + listener = soup_socket_new (SOUP_SOCKET_LOCAL_ADDRESS, address, + SOUP_SOCKET_IPV6_ONLY, ipv6_only, + NULL); + + success = soup_server_listen_internal (server, listener, options, error); + g_object_unref (listener); + + return success; +} + +static gboolean +soup_server_listen_ipv4_ipv6 (SoupServer *server, + GInetAddress *iaddr4, + GInetAddress *iaddr6, + guint port, + SoupServerListenOptions options, + GError **error) +{ + SoupServerPrivate *priv = soup_server_get_instance_private (server); + GSocketAddress *addr4, *addr6; + GError *my_error = NULL; + SoupSocket *v4sock; + guint v4port; + + g_return_val_if_fail (iaddr4 != NULL || iaddr6 != NULL, FALSE); + + options &= ~(SOUP_SERVER_LISTEN_IPV4_ONLY | SOUP_SERVER_LISTEN_IPV6_ONLY); + + try_again: + if (iaddr4) { + addr4 = g_inet_socket_address_new (iaddr4, port); + if (!soup_server_listen (server, addr4, options, error)) { + g_object_unref (addr4); + return FALSE; + } + g_object_unref (addr4); + + v4sock = priv->listeners->data; + v4port = g_inet_socket_address_get_port (soup_socket_get_local_address (v4sock)); + } else { + v4sock = NULL; + v4port = port; + } + + if (!iaddr6) + return TRUE; + + addr6 = g_inet_socket_address_new (iaddr6, v4port); + if (soup_server_listen (server, addr6, options, &my_error)) { + g_object_unref (addr6); + return TRUE; + } + g_object_unref (addr6); + + if (v4sock && g_error_matches (my_error, G_IO_ERROR, +#if GLIB_CHECK_VERSION (2, 41, 0) + G_IO_ERROR_NOT_SUPPORTED +#else + G_IO_ERROR_FAILED +#endif + )) { + /* No IPv6 support, but IPV6_ONLY wasn't specified, so just + * ignore the failure. + */ + g_error_free (my_error); + return TRUE; + } + + if (v4sock) { + priv->listeners = g_slist_remove (priv->listeners, v4sock); + soup_socket_disconnect (v4sock); + g_object_unref (v4sock); + } + + if (port == 0 && g_error_matches (my_error, G_IO_ERROR, G_IO_ERROR_ADDRESS_IN_USE)) { + /* The randomly-assigned IPv4 port was in use on the IPv6 side... Try again */ + g_clear_error (&my_error); + goto try_again; + } + + g_propagate_error (error, my_error); + return FALSE; +} + +/** + * soup_server_listen_all: + * @server: a #SoupServer + * @port: the port to listen on, or 0 + * @options: listening options for this server + * @error: return location for a #GError + * + * This attempts to set up @server to listen for connections on all + * interfaces on the system. (That is, it listens on the addresses + * <literal>0.0.0.0</literal> and/or <literal>::</literal>, depending + * on whether @options includes %SOUP_SERVER_LISTEN_IPV4_ONLY, + * %SOUP_SERVER_LISTEN_IPV6_ONLY, or neither.) If @port is specified, + * @server will listen on that port. If it is 0, @server will find an + * unused port to listen on. (In that case, you can use + * soup_server_get_uris() to find out what port it ended up choosing.) + * + * See soup_server_listen() for more details. + * + * Return value: %TRUE on success, %FALSE if @port could not be bound + * or any other error occurred (in which case @error will be set). + * + * Since: 2.48 + **/ +gboolean +soup_server_listen_all (SoupServer *server, guint port, + SoupServerListenOptions options, + GError **error) +{ + GInetAddress *iaddr4, *iaddr6; + gboolean success; + + g_return_val_if_fail (SOUP_IS_SERVER (server), FALSE); + g_return_val_if_fail (!(options & SOUP_SERVER_LISTEN_IPV4_ONLY) || + !(options & SOUP_SERVER_LISTEN_IPV6_ONLY), FALSE); + + if (options & SOUP_SERVER_LISTEN_IPV6_ONLY) + iaddr4 = NULL; + else + iaddr4 = g_inet_address_new_any (G_SOCKET_FAMILY_IPV4); + + if (options & SOUP_SERVER_LISTEN_IPV4_ONLY) + iaddr6 = NULL; + else + iaddr6 = g_inet_address_new_any (G_SOCKET_FAMILY_IPV6); + + success = soup_server_listen_ipv4_ipv6 (server, iaddr4, iaddr6, + port, options, error); + + g_clear_object (&iaddr4); + g_clear_object (&iaddr6); + + return success; +} + +/** + * soup_server_listen_local: + * @server: a #SoupServer + * @port: the port to listen on, or 0 + * @options: listening options for this server + * @error: return location for a #GError + * + * This attempts to set up @server to listen for connections on + * "localhost" (that is, <literal>127.0.0.1</literal> and/or + * <literal>::1</literal>, depending on whether @options includes + * %SOUP_SERVER_LISTEN_IPV4_ONLY, %SOUP_SERVER_LISTEN_IPV6_ONLY, or + * neither). If @port is specified, @server will listen on that port. + * If it is 0, @server will find an unused port to listen on. (In that + * case, you can use soup_server_get_uris() to find out what port it + * ended up choosing.) + * + * See soup_server_listen() for more details. + * + * Return value: %TRUE on success, %FALSE if @port could not be bound + * or any other error occurred (in which case @error will be set). + * + * Since: 2.48 + **/ +gboolean +soup_server_listen_local (SoupServer *server, guint port, + SoupServerListenOptions options, + GError **error) +{ + GInetAddress *iaddr4, *iaddr6; + gboolean success; + + g_return_val_if_fail (SOUP_IS_SERVER (server), FALSE); + g_return_val_if_fail (!(options & SOUP_SERVER_LISTEN_IPV4_ONLY) || + !(options & SOUP_SERVER_LISTEN_IPV6_ONLY), FALSE); + + if (options & SOUP_SERVER_LISTEN_IPV6_ONLY) + iaddr4 = NULL; + else + iaddr4 = g_inet_address_new_loopback (G_SOCKET_FAMILY_IPV4); + + if (options & SOUP_SERVER_LISTEN_IPV4_ONLY) + iaddr6 = NULL; + else + iaddr6 = g_inet_address_new_loopback (G_SOCKET_FAMILY_IPV6); + + success = soup_server_listen_ipv4_ipv6 (server, iaddr4, iaddr6, + port, options, error); + + g_clear_object (&iaddr4); + g_clear_object (&iaddr6); + + return success; +} + +/** + * soup_server_listen_socket: + * @server: a #SoupServer + * @socket: a listening #GSocket + * @options: listening options for this server + * @error: return location for a #GError + * + * This attempts to set up @server to listen for connections on + * @socket. + * + * See soup_server_listen() for more details. + * + * Return value: %TRUE on success, %FALSE if an error occurred (in + * which case @error will be set). + * + * Since: 2.48 + **/ +gboolean +soup_server_listen_socket (SoupServer *server, GSocket *socket, + SoupServerListenOptions options, + GError **error) +{ + SoupServerPrivate *priv; + SoupSocket *listener; + gboolean success; + + g_return_val_if_fail (SOUP_IS_SERVER (server), FALSE); + g_return_val_if_fail (G_IS_SOCKET (socket), FALSE); + g_return_val_if_fail (!(options & SOUP_SERVER_LISTEN_IPV4_ONLY) && + !(options & SOUP_SERVER_LISTEN_IPV6_ONLY), FALSE); + + priv = soup_server_get_instance_private (server); + g_return_val_if_fail (priv->disposed == FALSE, FALSE); + + listener = g_initable_new (SOUP_TYPE_SOCKET, NULL, error, + SOUP_SOCKET_GSOCKET, socket, + SOUP_SOCKET_IPV6_ONLY, TRUE, + NULL); + if (!listener) + return FALSE; + + success = soup_server_listen_internal (server, listener, options, error); + g_object_unref (listener); + + return success; +} + +/** + * soup_server_get_uris: + * @server: a #SoupServer + * + * Gets a list of URIs corresponding to the interfaces @server is + * listening on. These will contain IP addresses, not hostnames, and + * will also indicate whether the given listener is http or https. + * + * Note that if you used soup_server_listen_all(), the returned URIs + * will use the addresses <literal>0.0.0.0</literal> and + * <literal>::</literal>, rather than actually returning separate URIs + * for each interface on the system. + * + * Return value: (transfer full) (element-type Soup.URI): a list of + * #SoupURIs, which you must free when you are done with it. + * + * Since: 2.48 + */ +GSList * +soup_server_get_uris (SoupServer *server) +{ + SoupServerPrivate *priv; + GSList *uris, *l; + SoupSocket *listener; + GInetSocketAddress *addr; + GInetAddress *inet_addr; + char *ip; + SoupURI *uri; + gpointer creds; + + g_return_val_if_fail (SOUP_IS_SERVER (server), NULL); + priv = soup_server_get_instance_private (server); + + for (l = priv->listeners, uris = NULL; l; l = l->next) { + listener = l->data; + addr = soup_socket_get_local_address (listener); + inet_addr = g_inet_socket_address_get_address (addr); + ip = g_inet_address_to_string (inet_addr); + g_object_get (G_OBJECT (listener), SOUP_SOCKET_SSL_CREDENTIALS, &creds, NULL); + + uri = soup_uri_new (NULL); + soup_uri_set_scheme (uri, creds ? "https" : "http"); + soup_uri_set_host (uri, ip); + soup_uri_set_port (uri, g_inet_socket_address_get_port (addr)); + soup_uri_set_path (uri, "/"); + + uris = g_slist_prepend (uris, uri); + + g_free (ip); + } + + return uris; +} + +/** + * SoupClientContext: + * + * A #SoupClientContext provides additional information about the + * client making a particular request. In particular, you can use + * soup_client_context_get_auth_domain() and + * soup_client_context_get_auth_user() to determine if HTTP + * authentication was used successfully. + * + * soup_client_context_get_remote_address() and/or + * soup_client_context_get_host() can be used to get information for + * logging or debugging purposes. soup_client_context_get_socket() may + * also be of use in some situations (eg, tracking when multiple + * requests are made on the same connection). + **/ +G_DEFINE_BOXED_TYPE (SoupClientContext, soup_client_context, soup_client_context_ref, soup_client_context_unref) + +/** + * soup_client_context_get_soup_socket: + * @client: a #SoupClientContext + * + * Retrieves the #SoupSocket that @client is associated with. + * + * If you are using this method to observe when multiple requests are + * made on the same persistent HTTP connection (eg, as the ntlm-test + * test program does), you will need to pay attention to socket + * destruction as well (either by using weak references, or by + * connecting to the #SoupSocket::disconnected signal), so that you do + * not get fooled when the allocator reuses the memory address of a + * previously-destroyed socket to represent a new socket. + * + * Return value: (transfer none): the #SoupSocket that @client is + * associated with. + **/ +SoupSocket * +soup_client_context_get_soup_socket (SoupClientContext *client) +{ + g_return_val_if_fail (client != NULL, NULL); + + return client->sock; +} + +/** + * soup_client_context_get_socket: + * @client: a #SoupClientContext + * + * Retrieves the #GSocket that @client is associated with. + * + * If you are using this method to observe when multiple requests are + * made on the same persistent HTTP connection (eg, as the ntlm-test + * test program does), you will need to pay attention to socket + * destruction as well (eg, by using weak references), so that you do + * not get fooled when the allocator reuses the memory address of a + * previously-destroyed socket to represent a new socket. + * + * Return value: (nullable) (transfer none): the #GSocket that @client is + * associated with, %NULL if you used soup_server_accept_iostream(). + * + * Since: 2.48 + **/ +GSocket * +soup_client_context_get_socket (SoupClientContext *client) +{ + g_return_val_if_fail (client != NULL, NULL); + + return client->gsock; +} + +/** + * soup_client_context_get_remote_address: + * @client: a #SoupClientContext + * + * Retrieves the #GSocketAddress associated with the remote end + * of a connection. + * + * Return value: (nullable) (transfer none): the #GSocketAddress + * associated with the remote end of a connection, it may be + * %NULL if you used soup_server_accept_iostream(). + * + * Since: 2.48 + **/ +GSocketAddress * +soup_client_context_get_remote_address (SoupClientContext *client) +{ + g_return_val_if_fail (client != NULL, NULL); + + if (client->remote_addr) + return client->remote_addr; + + client->remote_addr = client->gsock ? + g_socket_get_remote_address (client->gsock, NULL) : + G_SOCKET_ADDRESS (g_object_ref (soup_socket_get_remote_address (client->sock))); + + return client->remote_addr; +} + +/** + * soup_client_context_get_local_address: + * @client: a #SoupClientContext + * + * Retrieves the #GSocketAddress associated with the local end + * of a connection. + * + * Return value: (nullable) (transfer none): the #GSocketAddress + * associated with the local end of a connection, it may be + * %NULL if you used soup_server_accept_iostream(). + * + * Since: 2.48 + **/ +GSocketAddress * +soup_client_context_get_local_address (SoupClientContext *client) +{ + g_return_val_if_fail (client != NULL, NULL); + + if (client->local_addr) + return client->local_addr; + + client->local_addr = client->gsock ? + g_socket_get_local_address (client->gsock, NULL) : + G_SOCKET_ADDRESS (g_object_ref (soup_socket_get_local_address (client->sock))); + + return client->local_addr; +} + +/** + * soup_client_context_get_host: + * @client: a #SoupClientContext + * + * Retrieves the IP address associated with the remote end of a + * connection. + * + * Return value: (nullable): the IP address associated with the remote + * end of a connection, it may be %NULL if you used + * soup_server_accept_iostream(). + **/ +const char * +soup_client_context_get_host (SoupClientContext *client) +{ + g_return_val_if_fail (client != NULL, NULL); + + if (client->remote_ip) + return client->remote_ip; + + if (client->gsock) { + GSocketAddress *addr = soup_client_context_get_remote_address (client); + GInetAddress *iaddr; + + if (!addr || !G_IS_INET_SOCKET_ADDRESS (addr)) + return NULL; + iaddr = g_inet_socket_address_get_address (G_INET_SOCKET_ADDRESS (addr)); + client->remote_ip = g_inet_address_to_string (iaddr); + } else { + GInetSocketAddress *addr = G_INET_SOCKET_ADDRESS (soup_socket_get_remote_address (client->sock)); + GInetAddress *inet_addr = g_inet_socket_address_get_address (addr); + client->remote_ip = g_inet_address_to_string (inet_addr); + } + + return client->remote_ip; +} + +/** + * soup_client_context_get_auth_domain: + * @client: a #SoupClientContext + * + * Checks whether the request associated with @client has been + * authenticated, and if so returns the #SoupAuthDomain that + * authenticated it. + * + * Return value: (transfer none) (nullable): a #SoupAuthDomain, or + * %NULL if the request was not authenticated. + **/ +SoupAuthDomain * +soup_client_context_get_auth_domain (SoupClientContext *client) +{ + g_return_val_if_fail (client != NULL, NULL); + + return client->auth_domain; +} + +/** + * soup_client_context_get_auth_user: + * @client: a #SoupClientContext + * + * Checks whether the request associated with @client has been + * authenticated, and if so returns the username that the client + * authenticated as. + * + * Return value: (nullable): the authenticated-as user, or %NULL if + * the request was not authenticated. + **/ +const char * +soup_client_context_get_auth_user (SoupClientContext *client) +{ + g_return_val_if_fail (client != NULL, NULL); + + return client->auth_user; +} + +/** + * soup_client_context_steal_connection: + * @client: a #SoupClientContext + * + * "Steals" the HTTP connection associated with @client from its + * #SoupServer. This happens immediately, regardless of the current + * state of the connection; if the response to the current + * #SoupMessage has not yet finished being sent, then it will be + * discarded; you can steal the connection from a + * #SoupMessage::wrote-informational or #SoupMessage::wrote-body signal + * handler if you need to wait for part or all of the response to be + * sent. + * + * Note that when calling this function from C, @client will most + * likely be freed as a side effect. + * + * Return value: (transfer full): the #GIOStream formerly associated + * with @client (or %NULL if @client was no longer associated with a + * connection). No guarantees are made about what kind of #GIOStream + * is returned. + * + * Since: 2.50 + **/ +GIOStream * +soup_client_context_steal_connection (SoupClientContext *client) +{ + GIOStream *stream; + + g_return_val_if_fail (client != NULL, NULL); + + soup_client_context_ref (client); + + stream = soup_message_io_steal (client->msg); + if (stream) { + g_object_set_data_full (G_OBJECT (stream), "GSocket", + soup_socket_steal_gsocket (client->sock), + g_object_unref); + } + + socket_disconnected (client->sock, client); + soup_client_context_unref (client); + + return stream; +} + + +/** + * SoupServerCallback: + * @server: the #SoupServer + * @msg: the message being processed + * @path: the path component of @msg's Request-URI + * @query: (element-type utf8 utf8) (allow-none): the parsed query + * component of @msg's Request-URI + * @client: additional contextual information about the client + * @user_data: the data passed to soup_server_add_handler() or + * soup_server_add_early_handler(). + * + * A callback used to handle requests to a #SoupServer. + * + * @path and @query contain the likewise-named components of the + * Request-URI, subject to certain assumptions. By default, + * #SoupServer decodes all percent-encoding in the URI path, such that + * "/foo%<!-- -->2Fbar" is treated the same as "/foo/bar". If your + * server is serving resources in some non-POSIX-filesystem namespace, + * you may want to distinguish those as two distinct paths. In that + * case, you can set the %SOUP_SERVER_RAW_PATHS property when creating + * the #SoupServer, and it will leave those characters undecoded. (You + * may want to call soup_uri_normalize() to decode any percent-encoded + * characters that you aren't handling specially.) + * + * @query contains the query component of the Request-URI parsed + * according to the rules for HTML form handling. Although this is the + * only commonly-used query string format in HTTP, there is nothing + * that actually requires that HTTP URIs use that format; if your + * server needs to use some other format, you can just ignore @query, + * and call soup_message_get_uri() and parse the URI's query field + * yourself. + * + * See soup_server_add_handler() and soup_server_add_early_handler() + * for details of what handlers can/should do. + **/ + +static SoupServerHandler * +get_or_create_handler (SoupServer *server, const char *exact_path) +{ + SoupServerPrivate *priv = soup_server_get_instance_private (server); + SoupServerHandler *handler; + + exact_path = NORMALIZED_PATH (exact_path); + + handler = soup_path_map_lookup (priv->handlers, exact_path); + if (handler && !strcmp (handler->path, exact_path)) + return handler; + + handler = g_slice_new0 (SoupServerHandler); + handler->path = g_strdup (exact_path); + soup_path_map_add (priv->handlers, exact_path, handler); + + return handler; +} + +/** + * soup_server_add_handler: + * @server: a #SoupServer + * @path: (allow-none): the toplevel path for the handler + * @callback: callback to invoke for requests under @path + * @user_data: data for @callback + * @destroy: destroy notifier to free @user_data + * + * Adds a handler to @server for requests under @path. If @path is + * %NULL or "/", then this will be the default handler for all + * requests that don't have a more specific handler. (Note though that + * if you want to handle requests to the special "*" URI, you must + * explicitly register a handler for "*"; the default handler will not + * be used for that case.) + * + * For requests under @path (that have not already been assigned a + * status code by a #SoupAuthDomain, an early #SoupServerHandler, or a + * signal handler), @callback will be invoked after receiving the + * request body; the message's #SoupMessage:method, + * #SoupMessage:request-headers, and #SoupMessage:request-body fields + * will be filled in. + * + * After determining what to do with the request, the callback must at + * a minimum call soup_message_set_status() (or + * soup_message_set_status_full()) on the message to set the response + * status code. Additionally, it may set response headers and/or fill + * in the response body. + * + * If the callback cannot fully fill in the response before returning + * (eg, if it needs to wait for information from a database, or + * another network server), it should call soup_server_pause_message() + * to tell @server to not send the response right away. When the + * response is ready, call soup_server_unpause_message() to cause it + * to be sent. + * + * To send the response body a bit at a time using "chunked" encoding, + * first call soup_message_headers_set_encoding() to set + * %SOUP_ENCODING_CHUNKED on the #SoupMessage:response-headers. Then call + * soup_message_body_append() (or soup_message_body_append_bytes)) + * to append each chunk as it becomes ready, and + * soup_server_unpause_message() to make sure it's running. (The + * server will automatically pause the message if it is using chunked + * encoding but no more chunks are available.) When you are done, call + * soup_message_body_complete() to indicate that no more chunks are + * coming. + **/ +void +soup_server_add_handler (SoupServer *server, + const char *path, + SoupServerCallback callback, + gpointer user_data, + GDestroyNotify destroy) +{ + SoupServerHandler *handler; + + g_return_if_fail (SOUP_IS_SERVER (server)); + g_return_if_fail (callback != NULL); + + handler = get_or_create_handler (server, path); + if (handler->destroy) + handler->destroy (handler->user_data); + + handler->callback = callback; + handler->destroy = destroy; + handler->user_data = user_data; +} + +/** + * soup_server_add_early_handler: + * @server: a #SoupServer + * @path: (allow-none): the toplevel path for the handler + * @callback: callback to invoke for requests under @path + * @user_data: data for @callback + * @destroy: destroy notifier to free @user_data + * + * Adds an "early" handler to @server for requests under @path. Note + * that "normal" and "early" handlers are matched up together, so if + * you add a normal handler for "/foo" and an early handler for + * "/foo/bar", then a request to "/foo/bar" (or any path below it) + * will run only the early handler. (But if you add both handlers at + * the same path, then both will get run.) + * + * For requests under @path (that have not already been assigned a + * status code by a #SoupAuthDomain or a signal handler), @callback + * will be invoked after receiving the request headers, but before + * receiving the request body; the message's #SoupMessage:method and + * #SoupMessage:request-headers fields will be filled in. + * + * Early handlers are generally used for processing requests with + * request bodies in a streaming fashion. If you determine that the + * request will contain a message body, normally you would call + * soup_message_body_set_accumulate() on the message's + * #SoupMessage:request-body to turn off request-body accumulation, + * and connect to the message's #SoupMessage::got-chunk signal to + * process each chunk as it comes in. + * + * To complete the message processing after the full message body has + * been read, you can either also connect to #SoupMessage::got-body, + * or else you can register a non-early handler for @path as well. As + * long as you have not set the #SoupMessage:status-code by the time + * #SoupMessage::got-body is emitted, the non-early handler will be + * run as well. + * + * Since: 2.50 + **/ +void +soup_server_add_early_handler (SoupServer *server, + const char *path, + SoupServerCallback callback, + gpointer user_data, + GDestroyNotify destroy) +{ + SoupServerHandler *handler; + + g_return_if_fail (SOUP_IS_SERVER (server)); + g_return_if_fail (callback != NULL); + + handler = get_or_create_handler (server, path); + if (handler->early_destroy) + handler->early_destroy (handler->early_user_data); + + handler->early_callback = callback; + handler->early_destroy = destroy; + handler->early_user_data = user_data; +} + +/** + * SoupServerWebsocketCallback: + * @server: the #SoupServer + * @path: the path component of @msg's Request-URI + * @connection: the newly created WebSocket connection + * @client: additional contextual information about the client + * @user_data: the data passed to @soup_server_add_handler + * + * A callback used to handle WebSocket requests to a #SoupServer. The + * callback will be invoked after sending the handshake response back + * to the client (and is only invoked if the handshake was + * successful). + * + * @path contains the path of the Request-URI, subject to the same + * rules as #SoupServerCallback (qv). + **/ + +/** + * soup_server_add_websocket_handler: + * @server: a #SoupServer + * @path: (allow-none): the toplevel path for the handler + * @origin: (allow-none): the origin of the connection + * @protocols: (allow-none) (array zero-terminated=1): the protocols + * supported by this handler + * @callback: callback to invoke for successful WebSocket requests under @path + * @user_data: data for @callback + * @destroy: destroy notifier to free @user_data + * + * Adds a WebSocket handler to @server for requests under @path. (If + * @path is %NULL or "/", then this will be the default handler for + * all requests that don't have a more specific handler.) + * + * When a path has a WebSocket handler registered, @server will check + * incoming requests for WebSocket handshakes after all other handlers + * have run (unless some earlier handler has already set a status code + * on the message), and update the request's status, response headers, + * and response body accordingly. + * + * If @origin is non-%NULL, then only requests containing a matching + * "Origin" header will be accepted. If @protocols is non-%NULL, then + * only requests containing a compatible "Sec-WebSocket-Protocols" + * header will be accepted. More complicated requirements can be + * handled by adding a normal handler to @path, and having it perform + * whatever checks are needed and + * setting a failure status code if the handshake should be rejected. + **/ +void +soup_server_add_websocket_handler (SoupServer *server, + const char *path, + const char *origin, + char **protocols, + SoupServerWebsocketCallback callback, + gpointer user_data, + GDestroyNotify destroy) +{ + SoupServerHandler *handler; + + g_return_if_fail (SOUP_IS_SERVER (server)); + g_return_if_fail (callback != NULL); + + handler = get_or_create_handler (server, path); + if (handler->websocket_destroy) + handler->websocket_destroy (handler->websocket_user_data); + if (handler->websocket_origin) + g_free (handler->websocket_origin); + if (handler->websocket_protocols) + g_strfreev (handler->websocket_protocols); + g_list_free_full (handler->websocket_extensions, g_object_unref); + + handler->websocket_callback = callback; + handler->websocket_destroy = destroy; + handler->websocket_user_data = user_data; + handler->websocket_origin = g_strdup (origin); + handler->websocket_protocols = g_strdupv (protocols); + handler->websocket_extensions = NULL; +} + +/** + * soup_server_remove_handler: + * @server: a #SoupServer + * @path: the toplevel path for the handler + * + * Removes all handlers (early and normal) registered at @path. + **/ +void +soup_server_remove_handler (SoupServer *server, const char *path) +{ + SoupServerPrivate *priv; + + g_return_if_fail (SOUP_IS_SERVER (server)); + priv = soup_server_get_instance_private (server); + + soup_path_map_remove (priv->handlers, NORMALIZED_PATH (path)); +} + +/** + * soup_server_add_auth_domain: + * @server: a #SoupServer + * @auth_domain: a #SoupAuthDomain + * + * Adds an authentication domain to @server. Each auth domain will + * have the chance to require authentication for each request that + * comes in; normally auth domains will require authentication for + * requests on certain paths that they have been set up to watch, or + * that meet other criteria set by the caller. If an auth domain + * determines that a request requires authentication (and the request + * doesn't contain authentication), @server will automatically reject + * the request with an appropriate status (401 Unauthorized or 407 + * Proxy Authentication Required). If the request used the + * "100-continue" Expectation, @server will reject it before the + * request body is sent. + **/ +void +soup_server_add_auth_domain (SoupServer *server, SoupAuthDomain *auth_domain) +{ + SoupServerPrivate *priv; + + g_return_if_fail (SOUP_IS_SERVER (server)); + priv = soup_server_get_instance_private (server); + + priv->auth_domains = g_slist_append (priv->auth_domains, auth_domain); + g_object_ref (auth_domain); +} + +/** + * soup_server_remove_auth_domain: + * @server: a #SoupServer + * @auth_domain: a #SoupAuthDomain + * + * Removes @auth_domain from @server. + **/ +void +soup_server_remove_auth_domain (SoupServer *server, SoupAuthDomain *auth_domain) +{ + SoupServerPrivate *priv; + + g_return_if_fail (SOUP_IS_SERVER (server)); + priv = soup_server_get_instance_private (server); + + priv->auth_domains = g_slist_remove (priv->auth_domains, auth_domain); + g_object_unref (auth_domain); +} + +/** + * soup_server_pause_message: + * @server: a #SoupServer + * @msg: a #SoupMessage associated with @server. + * + * Pauses I/O on @msg. This can be used when you need to return from + * the server handler without having the full response ready yet. Use + * soup_server_unpause_message() to resume I/O. + * + * This must only be called on a #SoupMessage which was created by the + * #SoupServer and are currently doing I/O, such as those passed into a + * #SoupServerCallback or emitted in a #SoupServer::request-read signal. + **/ +void +soup_server_pause_message (SoupServer *server, + SoupMessage *msg) +{ + g_return_if_fail (SOUP_IS_SERVER (server)); + g_return_if_fail (SOUP_IS_MESSAGE (msg)); + + soup_message_io_pause (msg); +} + +/** + * soup_server_unpause_message: + * @server: a #SoupServer + * @msg: a #SoupMessage associated with @server. + * + * Resumes I/O on @msg. Use this to resume after calling + * soup_server_pause_message(), or after adding a new chunk to a + * chunked response. + * + * I/O won't actually resume until you return to the main loop. + * + * This must only be called on a #SoupMessage which was created by the + * #SoupServer and are currently doing I/O, such as those passed into a + * #SoupServerCallback or emitted in a #SoupServer::request-read signal. + **/ +void +soup_server_unpause_message (SoupServer *server, + SoupMessage *msg) +{ + g_return_if_fail (SOUP_IS_SERVER (server)); + g_return_if_fail (SOUP_IS_MESSAGE (msg)); + + soup_message_io_unpause (msg); +} + +/** + * soup_server_add_websocket_extension: + * @server: a #SoupServer + * @extension_type: a #GType + * + * Add support for a WebSocket extension of the given @extension_type. + * When a WebSocket client requests an extension of @extension_type, + * a new #SoupWebsocketExtension of type @extension_type will be created + * to handle the request. + * + * You can also add support for a WebSocket extension to the server at + * construct time by using the %SOUP_SERVER_ADD_WEBSOCKET_EXTENSION property. + * Note that #SoupWebsocketExtensionDeflate is supported by default, use + * soup_server_remove_websocket_extension() if you want to disable it. + * + * Since: 2.68 + */ +void +soup_server_add_websocket_extension (SoupServer *server, GType extension_type) +{ + SoupServerPrivate *priv; + + g_return_if_fail (SOUP_IS_SERVER (server)); + + priv = soup_server_get_instance_private (server); + if (!g_type_is_a (extension_type, SOUP_TYPE_WEBSOCKET_EXTENSION)) { + g_warning ("Type '%s' is not a SoupWebsocketExtension", g_type_name (extension_type)); + return; + } + + g_ptr_array_add (priv->websocket_extension_types, g_type_class_ref (extension_type)); +} + +/** + * soup_server_remove_websocket_extension: + * @server: a #SoupServer + * @extension_type: a #GType + * + * Removes support for WebSocket extension of type @extension_type (or any subclass of + * @extension_type) from @server. You can also remove extensions enabled by default + * from the server at construct time by using the %SOUP_SERVER_REMOVE_WEBSOCKET_EXTENSION + * property. + * + * Since: 2.68 + */ +void +soup_server_remove_websocket_extension (SoupServer *server, GType extension_type) +{ + SoupServerPrivate *priv; + SoupWebsocketExtensionClass *extension_class; + guint i; + + g_return_if_fail (SOUP_IS_SERVER (server)); + + priv = soup_server_get_instance_private (server); + if (!g_type_is_a (extension_type, SOUP_TYPE_WEBSOCKET_EXTENSION)) { + g_warning ("Type '%s' is not a SoupWebsocketExtension", g_type_name (extension_type)); + return; + } + + extension_class = g_type_class_peek (extension_type); + for (i = 0; i < priv->websocket_extension_types->len; i++) { + if (priv->websocket_extension_types->pdata[i] == (gpointer)extension_class) { + g_ptr_array_remove_index (priv->websocket_extension_types, i); + break; + } + } +} diff --git a/libsoup/server/soup-server.h b/libsoup/server/soup-server.h new file mode 100644 index 00000000..eb9a13b7 --- /dev/null +++ b/libsoup/server/soup-server.h @@ -0,0 +1,180 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2000-2003, Ximian, Inc. + */ + +#pragma once + +#include "soup-types.h" +#include "soup-uri.h" +#include "soup-websocket-connection.h" + +G_BEGIN_DECLS + +#define SOUP_TYPE_SERVER (soup_server_get_type ()) +SOUP_AVAILABLE_IN_2_4 +G_DECLARE_DERIVABLE_TYPE (SoupServer, soup_server, SOUP, SERVER, GObject) + +typedef struct _SoupClientContext SoupClientContext; +SOUP_AVAILABLE_IN_2_4 +GType soup_client_context_get_type (void); +#define SOUP_TYPE_CLIENT_CONTEXT (soup_client_context_get_type ()) + +typedef enum { + SOUP_SERVER_LISTEN_HTTPS = (1 << 0), + SOUP_SERVER_LISTEN_IPV4_ONLY = (1 << 1), + SOUP_SERVER_LISTEN_IPV6_ONLY = (1 << 2) +} SoupServerListenOptions; + +struct _SoupServerClass { + GObjectClass parent_class; + + /* signals */ + void (*request_started) (SoupServer *server, SoupMessage *msg, + SoupClientContext *client); + void (*request_read) (SoupServer *server, SoupMessage *msg, + SoupClientContext *client); + void (*request_finished) (SoupServer *server, SoupMessage *msg, + SoupClientContext *client); + void (*request_aborted) (SoupServer *server, SoupMessage *msg, + SoupClientContext *client); + + gpointer padding[6]; +}; + +#define SOUP_SERVER_TLS_CERTIFICATE "tls-certificate" +#define SOUP_SERVER_RAW_PATHS "raw-paths" +#define SOUP_SERVER_SERVER_HEADER "server-header" +#define SOUP_SERVER_HTTP_ALIASES "http-aliases" +#define SOUP_SERVER_HTTPS_ALIASES "https-aliases" + +SOUP_AVAILABLE_IN_2_4 +SoupServer *soup_server_new (const char *optname1, + ...) G_GNUC_NULL_TERMINATED; + +SOUP_AVAILABLE_IN_2_48 +gboolean soup_server_set_ssl_cert_file (SoupServer *server, + const char *ssl_cert_file, + const char *ssl_key_file, + GError **error); +SOUP_AVAILABLE_IN_2_4 +gboolean soup_server_is_https (SoupServer *server); + +SOUP_AVAILABLE_IN_2_48 +gboolean soup_server_listen (SoupServer *server, + GSocketAddress *address, + SoupServerListenOptions options, + GError **error); +SOUP_AVAILABLE_IN_2_48 +gboolean soup_server_listen_all (SoupServer *server, + guint port, + SoupServerListenOptions options, + GError **error); +SOUP_AVAILABLE_IN_2_48 +gboolean soup_server_listen_local (SoupServer *server, + guint port, + SoupServerListenOptions options, + GError **error); +SOUP_AVAILABLE_IN_2_48 +gboolean soup_server_listen_socket (SoupServer *server, + GSocket *socket, + SoupServerListenOptions options, + GError **error); +SOUP_AVAILABLE_IN_2_48 +GSList *soup_server_get_uris (SoupServer *server); +SOUP_AVAILABLE_IN_2_48 +GSList *soup_server_get_listeners (SoupServer *server); + +SOUP_AVAILABLE_IN_2_4 +void soup_server_disconnect (SoupServer *server); + +SOUP_AVAILABLE_IN_2_50 +gboolean soup_server_accept_iostream (SoupServer *server, + GIOStream *stream, + GSocketAddress *local_addr, + GSocketAddress *remote_addr, + GError **error); + +/* Handlers and auth */ + +typedef void (*SoupServerCallback) (SoupServer *server, + SoupMessage *msg, + const char *path, + GHashTable *query, + SoupClientContext *client, + gpointer user_data); + +SOUP_AVAILABLE_IN_2_4 +void soup_server_add_handler (SoupServer *server, + const char *path, + SoupServerCallback callback, + gpointer user_data, + GDestroyNotify destroy); +SOUP_AVAILABLE_IN_2_50 +void soup_server_add_early_handler (SoupServer *server, + const char *path, + SoupServerCallback callback, + gpointer user_data, + GDestroyNotify destroy); + +#define SOUP_SERVER_ADD_WEBSOCKET_EXTENSION "add-websocket-extension" +#define SOUP_SERVER_REMOVE_WEBSOCKET_EXTENSION "remove-websocket-extension" + +typedef void (*SoupServerWebsocketCallback) (SoupServer *server, + SoupWebsocketConnection *connection, + const char *path, + SoupClientContext *client, + gpointer user_data); +SOUP_AVAILABLE_IN_2_50 +void soup_server_add_websocket_handler (SoupServer *server, + const char *path, + const char *origin, + char **protocols, + SoupServerWebsocketCallback callback, + gpointer user_data, + GDestroyNotify destroy); +SOUP_AVAILABLE_IN_2_68 +void soup_server_add_websocket_extension (SoupServer *server, + GType extension_type); +SOUP_AVAILABLE_IN_2_68 +void soup_server_remove_websocket_extension (SoupServer *server, + GType extension_type); + +SOUP_AVAILABLE_IN_2_4 +void soup_server_remove_handler (SoupServer *server, + const char *path); + +SOUP_AVAILABLE_IN_2_4 +void soup_server_add_auth_domain (SoupServer *server, + SoupAuthDomain *auth_domain); +SOUP_AVAILABLE_IN_2_4 +void soup_server_remove_auth_domain (SoupServer *server, + SoupAuthDomain *auth_domain); + +/* I/O */ +SOUP_AVAILABLE_IN_2_4 +void soup_server_pause_message (SoupServer *server, + SoupMessage *msg); +SOUP_AVAILABLE_IN_2_4 +void soup_server_unpause_message (SoupServer *server, + SoupMessage *msg); + +/* Client context */ + +SOUP_AVAILABLE_IN_2_48 +GSocket *soup_client_context_get_socket (SoupClientContext *client); +SOUP_AVAILABLE_IN_2_48 +GSocketAddress *soup_client_context_get_local_address (SoupClientContext *client); +SOUP_AVAILABLE_IN_2_48 +GSocketAddress *soup_client_context_get_remote_address (SoupClientContext *client); +SOUP_AVAILABLE_IN_2_4 +const char *soup_client_context_get_host (SoupClientContext *client); +SOUP_AVAILABLE_IN_2_4 +SoupAuthDomain *soup_client_context_get_auth_domain (SoupClientContext *client); +SOUP_AVAILABLE_IN_2_4 +const char *soup_client_context_get_auth_user (SoupClientContext *client); + +SOUP_AVAILABLE_IN_2_50 +GIOStream *soup_client_context_steal_connection (SoupClientContext *client); + +G_END_DECLS |
