* Copyright (C) 2013 Red Hat, Inc.
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#include "config.h"
#ifdef G_LOG_DOMAIN
#undef G_LOG_DOMAIN
#endif
#define G_LOG_DOMAIN "WebSocket"
#include "websocket.h"
#include "websocketprivate.h"
#include <stdlib.h>
#include <string.h>
* WebSocketState:
* @WEB_SOCKET_STATE_CONNECTING: the WebSocket is not yet ready to send messages
* @WEB_SOCKET_STATE_OPEN: the Websocket is ready to send messages
* @WEB_SOCKET_STATE_CLOSING: the Websocket is in the process of closing down, no further messages sent
* @WEB_SOCKET_STATE_CLOSED: the Websocket is completely closed down
*
* The WebSocket is in the %WEB_SOCKET_STATE_CONNECTING state during initial
* connection setup, and handshaking. If the handshake or connection fails it
* can go directly to the %WEB_SOCKET_STATE_CLOSED state from here.
*
* Once the WebSocket handshake completes successfully it will be in the
* %WEB_SOCKET_STATE_OPEN state. During this state, and only during this state
* can WebSocket messages be sent.
*
* WebSocket messages can be received during either the %WEB_SOCKET_STATE_OPEN
* or %WEB_SOCKET_STATE_CLOSING states.
*
* The WebSocket goes into the %WEB_SOCKET_STATE_CLOSING state once it has
* successfully sent a close request to the peer. If we had not yet received
* an earlier close request from the peer, then the WebSocket waits for a
* response to the close request (until a timeout).
*
* Once actually closed completely down the WebSocket state is
* %WEB_SOCKET_STATE_CLOSED. No communication is possible during this state.
*/
GQuark
web_socket_error_get_quark (void)
{
return g_quark_from_static_string ("web-socket-error-quark");
}
static inline const gchar *
strskip (const gchar *start,
const gchar c,
const gchar *end)
{
while (start != end && start[0] == c)
start++;
return start;
}
static gsize
parse_version (const gchar *data,
gsize length,
gchar **version)
{
if (length < 8)
return 0;
if (memcmp (data, "HTTP/1.0", 8) != 0 &&
memcmp (data, "HTTP/1.1", 8) != 0)
return 0;
if (version)
*version = g_strndup (data, 8);
return 8;
}
gboolean
_web_socket_util_parse_url (const gchar *url,
gchar **out_scheme,
gchar **out_host,
gchar **out_path,
GError **error)
{
const gchar *colon;
const gchar *host;
const gchar *path;
colon = strchr (url, ':');
if (colon == NULL ||
(colon[1] != '/' && colon[2] != '/'))
{
g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
"Invalid URI '%s'", url);
return FALSE;
}
path = strchr (colon + 3, '/');
host = strchr (colon + 3, '@');
if (host && (!path || host < path))
{
host++;
}
else
{
host = colon + 3;
path = strchr (host, '/');
}
if (host[0] == '\0' || host == path)
{
g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
"Invalid URI '%s'", url);
return FALSE;
}
if (out_scheme)
*out_scheme = g_strndup (url, colon - url);
if (out_host)
{
if (path)
*out_host = g_strndup (host, path - host);
else
*out_host = g_strdup (host);
}
if (out_path)
{
if (!path)
path = "/";
*out_path = g_strdup (path);
}
return TRUE;
}
static gboolean
is_valid_line (const gchar *string,
gssize length)
{
gint i;
if (length < 0)
length = strlen (string);
for (i = 0; i < length; i++)
{
if (string[i] != '\t')
{
if (string[i] < ' ' || string[i] & 0x80)
return FALSE;
}
}
return TRUE;
}
* web_socket_util_parse_req_line:
* @data: (array length=length): the input data
* @length: length of data
* @method: (out): location to place HTTP method, or %NULL
* @resource: (out): location to place HTTP resource path, or %NULL
*
* Parse an HTTP request line.
*
* The number of bytes parsed will be returned if parsing succeeds, including
* the new line at the end of the request line. A negative value will be
* returned if parsing fails.
*
* If the HTTP request line was truncated (ie: not all of it was present
* within @length) then zero will be returned.
*
* The @method and @resource should point to string pointers. The values
* returned should be freed by the caller using g_free().
*
* Return value: zero if truncated, negative if fails, or number of
* characters parsed
*/
gssize
web_socket_util_parse_req_line (const gchar *data,
gsize length,
gchar **method,
gchar **resource)
{
const gchar *end;
const gchar *method_end;
const gchar *path_beg;
const gchar *path_end;
const gchar *version;
const gchar *last;
gsize n;
* Here we parse a line like:
*
* GET /path/to/file HTTP/1.1
*/
g_return_val_if_fail (data != NULL || length == 0, -1);
if (length == 0)
return 0;
end = memchr (data, '\n', length);
if (end == NULL)
return 0;
if (data[0] == ' ')
return -1;
method_end = memchr (data, ' ', (end - data));
if (method_end == NULL)
return -1;
path_beg = strskip (method_end + 1, ' ', end);
path_end = memchr (path_beg, ' ', (end - path_beg));
if (path_end == NULL)
return -1;
version = strskip (path_end + 1, ' ', end);
n = parse_version (version, (end - version), NULL);
if (n == 0)
return -1;
last = version + n;
while (last != end)
{
if (!strchr ("\r ", last[0]))
return -1;
last++;
}
if (!is_valid_line (data, (method_end - data)) ||
!is_valid_line (data, (path_end - path_beg)))
return -1;
if (method)
*method = g_strndup (data, (method_end - data));
if (resource)
*resource = g_strndup (path_beg, (path_end - path_beg));
return (end - data) + 1;
}
static guint
str_case_hash (gconstpointer v)
{
const signed char *p;
guint32 h = 5381;
for (p = v; *p != '\0'; p++)
h = (h << 5) + h + g_ascii_tolower (*p);
return h;
}
static gboolean
str_case_equal (gconstpointer v1,
gconstpointer v2)
{
return g_ascii_strcasecmp (v1, v2) == 0;
}
* web_socket_util_new_headers:
*
* Create a new hashtable for HTTP headers.
*
* The GHashTable contains allocated null-terminated strings, as would
* be returned by g_strdup(). The headers are indexed by the header names
* in a case insensitive way.
*
* It is not necessary to worry about case headers in this GHashTable.
*
* Return value: (transfer full): a new header hashtable
*/
GHashTable *
web_socket_util_new_headers (void)
{
return g_hash_table_new_full (str_case_hash, str_case_equal, g_free, g_free);
}
* web_socket_util_parse_headers:
* @data: (array length=length): the input data
* @length: length of data
* @headers: (out): location to place HTTP header hash table
*
* Parse HTTP headers.
*
* The number of bytes parsed will be returned if parsing succeeds, including
* the new line at the end of the request line. A negative value will be
* returned if parsing fails.
*
* If the HTTP request line was truncated (ie: not all of it was present
* within @length) then zero will be returned.
*
* The @headers returned will be allocated using web_socket_util_new_headers(),
* and should be freed by the caller using g_free().
*
* Return value: zero if truncated, negative if fails, or number of
* characters parsed
*/
gssize
web_socket_util_parse_headers (const gchar *data,
gsize length,
GHashTable **headers)
{
GHashTable *parsed_headers;
const gchar *line;
const gchar *colon;
gsize consumed = 0;
gboolean end = FALSE;
gsize line_len;
parsed_headers = web_socket_util_new_headers ();
while (!end)
{
line = memchr (data, '\n', length);
if (line == NULL)
{
consumed = 0;
break;
}
line++;
line_len = (line - data);
if ((data[0] == '\r' && data[1] == '\n') || data[0] == '\n')
{
end = TRUE;
}
else
{
colon = memchr (data, ':', length);
if (!colon || colon >= line)
{
g_debug ("received invalid header line: %.*s", (gint)line_len, data);
consumed = -1;
break;
}
g_autofree gchar *name = g_strndup (data, colon - data);
g_strstrip (name);
g_autofree gchar *value = g_strndup (colon + 1, line - (colon + 1));
g_strstrip (value);
if (!is_valid_line (name, -1) || !g_utf8_validate (value, -1, NULL))
{
g_debug ("received invalid header");
consumed = -1;
break;
}
g_hash_table_insert (parsed_headers, g_steal_pointer (&name), g_steal_pointer (&value));
}
consumed += line_len;
data += line_len;
length -= line_len;
}
if (consumed > 0)
{
if (headers)
*headers = g_hash_table_ref (parsed_headers);
}
g_hash_table_unref (parsed_headers);
return consumed;
}
gboolean
_web_socket_util_header_equals (GHashTable *headers,
const gchar *name,
const gchar *want)
{
const gchar *value;
value = g_hash_table_lookup (headers, name);
if (value != NULL && g_ascii_strcasecmp (value, want) == 0)
return TRUE;
g_message ("received invalid or missing %s header: %s", name, value);
return FALSE;
}
gboolean
_web_socket_util_header_contains (GHashTable *headers,
const gchar *name,
const gchar *word)
{
const gchar *value;
const gchar *at;
value = g_hash_table_lookup (headers, name);
if (value != NULL)
{
at = strcasestr (value, word);
if (at != NULL &&
(at == value || !g_ascii_isalnum (*(at - 1))) &&
!g_ascii_isalnum (at[strlen (word)]))
return TRUE;
}
g_message ("received invalid or missing %s header: %s", name, value);
return FALSE;
}
gboolean
_web_socket_util_header_empty (GHashTable *headers,
const gchar *name)
{
const gchar *value;
value = g_hash_table_lookup (headers, name);
if (value == NULL || value[0] == '\0')
return TRUE;
g_message ("received unsupported %s header: %s", name, value);
return FALSE;
}
* web_socket_util_parse_status_line:
* @data: (array length=length): the input data
* @length: length of data
* @version: (out): location to place HTTP version, or %NULL
* @status: (out): location to place HTTP status, or %NULL
* @reason: (out): location to place HTTP message, or %NULL
*
* Parse an HTTP status line.
*
* The number of bytes parsed will be returned if parsing succeeds, including
* the new line at the end of the status line. A negative value will be
* returned if parsing fails.
*
* If the HTTP request line was truncated (ie: not all of it was present
* within @length) then zero will be returned.
*
* @reason should point to a string pointer. The value
* returned should be freed by the caller using g_free().
*
* Return value: zero if truncated, negative if fails, or number of
* characters parsed
*/
gssize
web_socket_util_parse_status_line (const gchar *data,
gsize length,
gchar **version,
guint *status,
gchar **reason)
{
const gchar *at;
const gchar *end;
gsize n;
guint64 num;
gchar *ep;
* Here we parse a line like:
*
* HTTP/1.1 101 Switching protocols
*/
at = data;
end = memchr (at, '\n', length);
if (end == NULL)
return 0;
n = parse_version (at, (end - at), version);
if (n == 0 || at[n] != ' ')
return -1;
at += n;
at = strskip (at, ' ', end);
if (memchr (at, ' ', (end - at)) == NULL)
return -1;
num = g_ascii_strtoull (at, &ep, 10);
if (num == 0 || num > G_MAXUINT || *ep != ' ')
return -1;
at = strskip (ep, ' ', end);
if (reason)
{
*reason = g_strndup (at, (end - at));
g_strstrip (*reason);
}
if (status)
*status = (guint)num;
return (end - data) + 1;
}