blob: 9bd92c8a4ce4620108d599c8032860c64f567d24 [file] [log] [blame] [edit]
/*
* Copyright (C) 2024 Igalia S.L.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#include "WebSocketServer.h"
#include "CommandResult.h"
#include "HTTPServer.h"
#include "Logging.h"
#include <cstdio>
#include <glib-object.h>
#include <libsoup/soup-websocket-connection.h>
#include <libsoup/soup.h>
#include <optional>
#include <span>
#include <tuple>
#include <wtf/glib/GUniquePtr.h>
#include <wtf/text/WTFString.h>
namespace WebDriver {
static bool soupServerListen(SoupServer* server, const String& host, unsigned port, GError** error)
{
static const auto options = static_cast<SoupServerListenOptions>(0);
if (!host || host == "local"_s)
return soup_server_listen_local(server, port, options, error);
if (host == "all"_s)
return soup_server_listen_all(server, port, options, error);
GRefPtr<GSocketAddress> address = adoptGRef(g_inet_socket_address_new_from_string(host.utf8().data(), port));
if (!address) {
g_set_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, "Invalid host IP address '%s'", host.utf8().data());
return false;
}
return soup_server_listen(server, address.get(), options, error);
}
static void handleIncomingHandshake(SoupServer*, SoupServerMessage* message, const char* path, GHashTable*, gpointer userData)
{
if (!soup_websocket_server_check_handshake(message, nullptr, nullptr, nullptr, nullptr)) {
soup_server_message_set_status(message, 500, "Invalid request");
return;
}
GRefPtr<SoupServerMessage> protectedMessage = message;
auto* requestBody = soup_server_message_get_request_body(message);
HTTPRequestHandler::Request handshakeMessage = {
String::fromUTF8(soup_server_message_get_method(message)),
String::fromUTF8(path),
requestBody->data,
static_cast<size_t>(requestBody->length)
};
auto* webSocketServer = static_cast<WebSocketServer*>(userData);
if (webSocketServer->messageHandler().acceptHandshake(WTF::move(handshakeMessage))) // Follow with handshake procedure
return;
HTTPRequestHandler::Response errorResponse = { 503, "Service Unavailable", "text/plain"_s };
RELEASE_LOG(WebDriverBiDi, "Error during handshake, sending error response: %s", errorResponse.data.data());
soup_server_message_set_status(message, errorResponse.statusCode, nullptr);
auto* responseHeaders = soup_server_message_get_response_headers(message);
soup_message_headers_append(responseHeaders, "Content-Type", errorResponse.contentType.utf8().data());
soup_message_headers_append(responseHeaders, "Cache-Control", "no-cache");
auto* responseBody = soup_server_message_get_response_body(message);
soup_message_body_append(responseBody, SOUP_MEMORY_COPY, errorResponse.data.data(), errorResponse.data.length());
}
static void handleWebSocketMessage(SoupWebsocketConnection* connection, SoupWebsocketDataType messageType, GBytes* message, gpointer userData)
{
// https://w3c.github.io/webdriver-bidi/#handle-an-incoming-message
if (messageType != SOUP_WEBSOCKET_DATA_TEXT) {
RELEASE_LOG(WebDriverBiDi, "websocket message handler received non-text message. error return");
auto errorReply = WebSocketMessageHandler::Message::fail(CommandResult::ErrorCode::InvalidArgument, std::nullopt, { "Non-text message received"_s });
GRefPtr<GBytes> rawMessage = adoptGRef(g_bytes_new(errorReply.payload.data(), errorReply.payload.length()));
soup_websocket_connection_send_message(connection, SOUP_WEBSOCKET_DATA_TEXT, rawMessage.get());
return;
}
auto webSocketServer = static_cast<WebSocketServer*>(userData);
gsize messageSize;
gconstpointer messageData = g_bytes_get_data(message, &messageSize);
WebSocketMessageHandler::Message messageObj = { connection, { std::span<const char>(static_cast<const char*>(messageData), messageSize) } };
webSocketServer->messageHandler().handleMessage(WTF::move(messageObj), [](WebSocketMessageHandler::Message&& message) {
if (!message.connection) {
RELEASE_LOG(WebDriverBiDi, "No connection found when trying to send message: %s", message.payload.data());
return;
}
GRefPtr<GBytes> rawMessage = adoptGRef(g_bytes_new(message.payload.data(), message.payload.length()));
// Using send_message to avoid dealing with null chars in the middle of the message
soup_websocket_connection_send_message(message.connection.get(), SOUP_WEBSOCKET_DATA_TEXT, rawMessage.get());
});
}
static void handleWebSocketConnection(SoupServer*, SoupServerMessage*, const char* path, SoupWebsocketConnection* connection, gpointer userData)
{
// Delayed steps from the end of the handshake, as now we have a connection object
auto webSocketServer = static_cast<WebSocketServer*>(userData);
if ("/session"_s == path)
webSocketServer->addStaticConnection({ connection });
else {
auto sessionId = webSocketServer->getSessionID(String::fromLatin1(path));
auto connectionForKey = WebSocketMessageHandler::Connection(connection);
webSocketServer->addConnection({ connection }, sessionId);
}
g_signal_connect(connection, "closed", G_CALLBACK(+[](SoupWebsocketConnection* connection, gpointer userData) {
auto webSocketServer = static_cast<WebSocketServer*>(userData);
webSocketServer->messageHandler().clientDisconnected(connection);
}), webSocketServer);
g_signal_connect(connection, "message", G_CALLBACK(handleWebSocketMessage), webSocketServer);
}
std::optional<String> WebSocketServer::listen(const String& host, unsigned port)
{
m_soupServer = adoptGRef(soup_server_new("server-header", "WebKitWebDriver-WSS", nullptr));
GUniqueOutPtr<GError> error;
if (!soupServerListen(m_soupServer.get(), host, port, &error.outPtr())) {
RELEASE_LOG(WebDriverBiDi, "Failed to start WebSocket server at port %u: %s", port, error->message);
return std::nullopt;
}
// Callback for handling incoming WebSocket handshake requests
soup_server_add_handler(m_soupServer.get(), nullptr, handleIncomingHandshake, this, nullptr);
// Callback called when the WebSocket handshake has finished and we have a valid WebSocket Connection object
soup_server_add_websocket_handler(m_soupServer.get(), nullptr, nullptr, nullptr, handleWebSocketConnection, this, nullptr);
// "/session" is the default resource to start bidi-only sessions
m_listener = WebSocketListener::create(
host.isNull() ? "localhost"_s : host,
port,
false,
{ "/session"_s }
);
return getWebSocketURL(m_listener, nullString());
}
void WebSocketServer::sendMessage(WebSocketMessageHandler::Connection connection, const String& message)
{
ASSERT(connection);
RELEASE_LOG(WebDriverBiDi, "Sending message: %s", message.utf8().data());
GRefPtr<GBytes> rawMessage = adoptGRef(g_bytes_new(message.utf8().data(), message.utf8().length()));
soup_websocket_connection_send_message(connection.get(), SOUP_WEBSOCKET_DATA_TEXT, rawMessage.get());
}
void WebSocketServer::disconnect()
{
if (!m_soupServer)
return;
for (const auto& connection : m_connectionToSession.keys()) {
if (!connection)
continue;
g_signal_handlers_disconnect_by_data(connection.get(), this);
}
soup_server_disconnect(m_soupServer.get());
m_soupServer = nullptr;
}
void WebSocketServer::disconnectSession(const String& sessionId)
{
auto connection = this->connection(sessionId);
if (!connection || !connection->get())
return;
soup_websocket_connection_close(connection->get(), SOUP_WEBSOCKET_CLOSE_NORMAL, nullptr);
g_signal_handlers_disconnect_by_data(connection->get(), this);
}
} // namespace WebDriver