/* * 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 "websocketserver.h" #include "websocketprivate.h" #include enum { PROP_0, PROP_ORIGINS, PROP_PROTOCOLS, PROP_REQUEST_HEADERS, PROP_INPUT_BUFFER, }; struct _WebSocketServer { WebSocketConnection parent; gboolean protocol_chosen; gchar **allowed_origins; gchar **allowed_protocols; GHashTable *request_headers; }; struct _WebSocketServerClass { WebSocketConnectionClass parent; }; G_DEFINE_TYPE (WebSocketServer, web_socket_server, WEB_SOCKET_TYPE_CONNECTION); static void web_socket_server_init (WebSocketServer *self) { } static void respond_handshake_forbidden (WebSocketConnection *conn) { GError *error; const gchar *bad_request = "HTTP/1.1 403 Forbidden\r\n" "Connection: close\r\n" "\r\n" "403 Forbidden\r\n" "Received invalid WebSocket request\r\n"; _web_socket_connection_queue (conn, WEB_SOCKET_QUEUE_URGENT | WEB_SOCKET_QUEUE_LAST, g_strdup (bad_request), strlen (bad_request), 0); g_debug ("queued: forbidden request response"); error = g_error_new_literal (WEB_SOCKET_ERROR, WEB_SOCKET_CLOSE_PROTOCOL, "Received invalid handshake request from the client"); _web_socket_connection_error (conn, error); } static void respond_handshake_bad (WebSocketConnection *conn) { GError *error; const gchar *bad_request = "HTTP/1.1 400 Bad Request\r\n" "Connection: close\r\n" "\r\n" "400 Bad Request\r\n" "Received invalid WebSocket request\r\n"; _web_socket_connection_queue (conn, WEB_SOCKET_QUEUE_URGENT | WEB_SOCKET_QUEUE_LAST, g_strdup (bad_request), strlen (bad_request), 0); g_debug ("queued: bad request response"); error = g_error_new_literal (WEB_SOCKET_ERROR, WEB_SOCKET_CLOSE_PROTOCOL, "Received invalid handshake request from the client"); _web_socket_connection_error (conn, error); } gchar * _web_socket_complete_accept_key_rfc6455 (const gchar *key) { gsize digest_len = 20; guchar digest[digest_len]; GChecksum *checksum; checksum = g_checksum_new (G_CHECKSUM_SHA1); g_return_val_if_fail (checksum != NULL, NULL); g_checksum_update (checksum, (guchar *)key, -1); /* magic from: http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-17 */ g_checksum_update (checksum, (guchar *)"258EAFA5-E914-47DA-95CA-C5AB0DC85B11", -1); g_checksum_get_digest (checksum, digest, &digest_len); g_checksum_free (checksum); g_assert (digest_len == 20); return g_base64_encode (digest, digest_len); } static gboolean validate_rfc6455_websocket_key (const gchar *key) { /* The key must be 16 bytes base64 encoded */ guchar *decoded; gsize length, len; len = strlen (key); if (len == 0 || len > 1024) return FALSE; decoded = g_base64_decode (key, &length); if (!decoded) return FALSE; g_free (decoded); return length == 16; } static gboolean respond_handshake_rfc6455 (WebSocketServer *self, WebSocketConnection *conn, GHashTable *headers) { const gchar *protocol; const gchar *origin; const gchar *host; gchar *accept_key; gchar *key; GString *handshake; gsize len; guint i; if (!_web_socket_util_header_equals (headers, "Upgrade", "websocket") || !_web_socket_util_header_contains (headers, "Connection", "upgrade") || !_web_socket_util_header_equals (headers, "Sec-WebSocket-Version", "13") || !_web_socket_connection_choose_protocol (conn, (const gchar **)self->allowed_protocols, g_hash_table_lookup (headers, "Sec-WebSocket-Protocol"))) { respond_handshake_bad (conn); return FALSE; } self->protocol_chosen = TRUE; key = g_hash_table_lookup (headers, "Sec-WebSocket-Key"); if (key == NULL) { g_message ("received missing Sec-WebSocket-Key header"); respond_handshake_bad (conn); return FALSE; } if (!validate_rfc6455_websocket_key (key)) { g_message ("received invalid Sec-WebSocket-Key header: %s", key); respond_handshake_bad (conn); return FALSE; } host = g_hash_table_lookup (headers, "Host"); if (host == NULL) { g_message ("received request without Host"); respond_handshake_bad (conn); return FALSE; } if (self->allowed_origins) { origin = g_hash_table_lookup (headers, "Origin"); if (!origin) { g_message ("received request without Origin"); respond_handshake_forbidden (conn); return FALSE; } for (i = 0; self->allowed_origins[i] != NULL; i++) { if (g_ascii_strcasecmp (origin, self->allowed_origins[i]) == 0) break; } if (self->allowed_origins[i] == NULL) { g_message ("received request from bad Origin: %s", origin); respond_handshake_forbidden (conn); return FALSE; } } accept_key = _web_socket_complete_accept_key_rfc6455 (key); handshake = g_string_new (""); g_string_printf (handshake, "HTTP/1.1 101 Switching Protocols\r\n" "Upgrade: websocket\r\n" "Connection: Upgrade\r\n" "Sec-WebSocket-Accept: %s\r\n", (gchar *)accept_key); g_free (accept_key); protocol = web_socket_connection_get_protocol (conn); if (protocol) g_string_append_printf (handshake, "Sec-WebSocket-Protocol: %s\r\n", protocol); g_string_append (handshake, "\r\n"); len = handshake->len; _web_socket_connection_queue (conn, WEB_SOCKET_QUEUE_URGENT, g_string_free (handshake, FALSE), len, 0); g_debug ("queued response to rfc6455 handshake"); return TRUE; } static gboolean parse_handshake_request (WebSocketServer *self, WebSocketConnection *conn, GByteArray *incoming) { GHashTable *headers; gchar *method; gchar *resource; gboolean valid; gssize in1, in2; gssize consumed; const gchar *url; /* Headers already passed from caller */ if (self->request_headers) { headers = self->request_headers; self->request_headers = NULL; method = g_strdup ("GET"); url = web_socket_connection_get_url (conn); if (!_web_socket_util_parse_url (url, NULL, NULL, &resource, NULL)) resource = g_strdup ("/"); consumed = 0; } else { /* Parse the handshake response received from the server */ in1 = web_socket_util_parse_req_line ((const gchar *)incoming->data, incoming->len, &method, &resource); if (in1 < 0) { g_message ("received invalid request line"); respond_handshake_bad (conn); } else if (in1 == 0) g_debug ("waiting for more handshake data"); if (in1 <= 0) return FALSE; /* Read in the handshake request from the client */ in2 = web_socket_util_parse_headers ((const gchar *)incoming->data + in1, incoming->len - in1, &headers); if (in2 < 0) { g_message ("received invalid response headers"); respond_handshake_bad (conn); } else if (in2 == 0) g_debug ("waiting for more handshake data"); if (in2 <= 0) return FALSE; consumed = in1 + in2; } if (!g_str_equal (method, "GET")) { g_message ("received unexpected method: %s %s", method, resource); valid = FALSE; } else { valid = respond_handshake_rfc6455 (self, conn, headers); } if (valid) { /* Handshake is successful */ g_debug ("open: responded to handshake"); } if (consumed > 0) g_byte_array_remove_range (incoming, 0, consumed); g_hash_table_unref (headers); g_free (resource); g_free (method); return valid; } static gboolean web_socket_server_handshake (WebSocketConnection *conn, GByteArray *incoming) { WebSocketServer *self = WEB_SOCKET_SERVER (conn); return parse_handshake_request (self, conn, incoming); } static void web_socket_server_constructed (GObject *object) { WebSocketConnection *conn = WEB_SOCKET_CONNECTION (object); GIOStream *io_stream; G_OBJECT_CLASS (web_socket_server_parent_class)->constructed (object); io_stream = web_socket_connection_get_io_stream (conn); if (io_stream == NULL) { g_critical ("server-side WebSocketConnection must be created " "with a io-stream property"); } } static void web_socket_server_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { WebSocketServer *self = WEB_SOCKET_SERVER (object); switch (prop_id) { case PROP_ORIGINS: g_return_if_fail (self->allowed_origins == FALSE); self->allowed_origins = g_value_dup_boxed (value); break; case PROP_PROTOCOLS: g_return_if_fail (self->protocol_chosen == FALSE); g_strfreev (self->allowed_protocols); self->allowed_protocols = g_value_dup_boxed (value); break; case PROP_REQUEST_HEADERS: g_return_if_fail (self->request_headers == NULL); self->request_headers = g_value_dup_boxed (value); break; case PROP_INPUT_BUFFER: _web_socket_connection_take_incoming (WEB_SOCKET_CONNECTION (self), g_value_dup_boxed (value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void web_socket_server_finalize (GObject *object) { WebSocketServer *self = WEB_SOCKET_SERVER (object); g_strfreev (self->allowed_origins); g_strfreev (self->allowed_protocols); if (self->request_headers) g_hash_table_unref (self->request_headers); G_OBJECT_CLASS (web_socket_server_parent_class)->finalize (object); } static void web_socket_server_class_init (WebSocketServerClass *klass) { WebSocketConnectionClass *conn_class = WEB_SOCKET_CONNECTION_CLASS (klass); GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->constructed = web_socket_server_constructed; object_class->set_property = web_socket_server_set_property; object_class->finalize = web_socket_server_finalize; conn_class->server_behavior = TRUE; conn_class->handshake = web_socket_server_handshake; /** * WebSocketServer:origins: * * The allowed origins to receive client requests from. */ g_object_class_install_property (object_class, PROP_ORIGINS, g_param_spec_boxed ("origins", "Possible Origins", "The possible HTTP origins", G_TYPE_STRV, G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS)); /** * WebSocketServer:protocols: * * The allowed protocols to negotiate with the client. */ g_object_class_install_property (object_class, PROP_PROTOCOLS, g_param_spec_boxed ("protocols", "Possible Protocol", "The possible WebSocket protocols", G_TYPE_STRV, G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS)); /** * WebSocketServer:request-headers: * * If headers have already been parsed, passed in here. */ g_object_class_install_property (object_class, PROP_REQUEST_HEADERS, g_param_spec_boxed ("request-headers", "Request Headers", "Already parsed headers", G_TYPE_HASH_TABLE, G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); /** * WebSocketServer:input-buffer: * * When specifying a #WebSocketConnection:io-stream during construction, * if you've already read bytes (ie: containing an HTTP header) out of the * input stream, then you must pass in a buffer containing those initial bytes, * so the WebSocket can consume them. * * This is usually only useful for WebSocket server connections. See * web_socket_server_new_for_stream() */ g_object_class_install_property (object_class, PROP_INPUT_BUFFER, g_param_spec_boxed ("input-buffer", "Input buffer", "Input buffer with seed data", G_TYPE_BYTE_ARRAY, G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); } /** * web_socket_server_new_for_stream: * @url: the url address of the WebSocket * @origins: (allow-none): the origin to expect the client to report * @protocols: (allow-none): possible protocols for the client to * @io_stream: the IO stream to communicate over * @request_headers: (allow-none): already parsed headers, or %NULL * @input_buffer: (allow-none): initial bytes already read from the input stream, or %NULL * * Create a new server side WebSocket connection to communicate with a client. * * Since callers may have already read some bytes from the inputstream (ie: * the HTTP header Request-Line) those bytes should be included in the * @input_buffer argument so that the WebSocket can consume them. * * If @protocols are specified then these are used to negotiate a protocol * with the client. * * The input and output streams of the @io_stream must be pollable. * * If the input stream on the @io_stream has already been read, those * read bytes should be passed in the @input_buffer byte array. * * In addition if the HTTP headers have already been parsed, they should be * passed in using the @request_headers hash table. This should be a hash table * setup for case-insensitive lookups, as created by web_socket_util_new_headers(). * When passing in headers, fill in @input_buffer with any of the HTTP body * read from the input stream (ie: after the \r\n\r\n). * * Returns: (transfer full): a new WebSocket */ WebSocketConnection * web_socket_server_new_for_stream (const gchar *url, const gchar **origins, const gchar **protocols, GIOStream *io_stream, GHashTable *request_headers, GByteArray *input_buffer) { return g_object_new (WEB_SOCKET_TYPE_SERVER, "url", url, "origins", origins, "protocols", protocols, "io-stream", io_stream, "request-headers", request_headers, "input-buffer", input_buffer, NULL); }