/*
* This file is part of Cockpit.
*
* Copyright (C) 2013 Red Hat, Inc.
*
* Cockpit is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* Cockpit 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 Cockpit; If not, see .
*/
#include "config.h"
#include "websocket.h"
#include "websocketprivate.h"
#include
#include
/**
* 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] != '/'))
{
/* The same error as g_network_address_parse_uri() */
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)
{
/* The same error as g_network_address_parse_uri() */
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;
}
/**
* 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; /* need more data */
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);
/* Returns number of characters consumed */
n = parse_version (version, (end - version), NULL);
if (n == 0)
return -1;
last = version + n;
while (last != end)
{
/* Acceptable trailing characters */
if (!strchr ("\r ", last[0]))
return -1;
last++;
}
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)
{
/* A case agnostic version of g_str_hash */
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)
{
/* A case agnostic version of g_str_equal */
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;
gchar *name;
gchar *value;
parsed_headers = web_socket_util_new_headers ();
while (!end)
{
line = memchr (data, '\n', length);
/* No line ending: need more data */
if (line == NULL)
{
consumed = 0;
break;
}
line++;
line_len = (line - data);
/* An empty line, all done */
if ((data[0] == '\r' && data[1] == '\n') || data[0] == '\n')
{
end = TRUE;
}
/* A header line */
else
{
colon = memchr (data, ':', length);
if (!colon || colon >= line)
{
g_message ("received invalid header line: %.*s", (gint)line_len, data);
consumed = -1;
break;
}
name = g_strndup (data, colon - data);
g_strstrip (name);
value = g_strndup (colon + 1, line - (colon + 1));
g_strstrip (value);
g_hash_table_insert (parsed_headers, name, 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)
{
/* The word must be present, and not part of another word */
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; /* need more data */
n = parse_version (at, (end - at), version);
if (n == 0 || at[n] != ' ')
return -1;
at += n;
/* Extra spaces */
at = strskip (at, ' ', end);
/* First check for space after status */
if (memchr (at, ' ', (end - at)) == NULL)
return -1;
/* This will stop at above space */
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;
}