blob: c4b48932af7ce63feeae99bf7f691b7d83c23a9d [file] [edit]
/*
* Copyright (C) 2026 Apple Inc. All rights reserved.
*
* 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 "HTTPServer.h"
#if PLATFORM(COCOA)
#include "Logging.h"
#include "LoopbackCocoa.h"
#include <CFNetwork/CFNetwork.h>
#include <Network/Network.h>
#include <wtf/JSONValues.h>
#include <wtf/OSObjectPtr.h>
#include <wtf/RefCountedAndCanMakeWeakPtr.h>
#include <wtf/RetainPtr.h>
#include <wtf/RunLoop.h>
#include <wtf/Vector.h>
#include <wtf/cf/VectorCF.h>
#include <wtf/darwin/DispatchExtras.h>
#include <wtf/text/MakeString.h>
#include <wtf/text/StringToIntegerConversion.h>
#include <wtf/text/WTFString.h>
// minidriver's HTTP server. Network.framework accepts the connections, and CFHTTPMessage parses
// requests and serializes responses. It listens on 127.0.0.1 and ::1, keeps connections alive and
// answers pipelined requests in order, and drops connections whose header is too large or whose
// request doesn't arrive in time. Before reading a body, it rejects requests whose Host or Origin
// isn't on this machine, and bodies that are too large.
namespace WebDriver {
// Room for 100 header lines of up to 8190 bytes. A connection whose request header is larger is
// dropped without a response.
static constexpr size_t maximumHeaderSize = 100 * 8190;
// The largest WebDriver requests carry script arguments or text to type. A larger body gets a 413.
static constexpr size_t maximumBodySize = 64 * MB;
// A connection that doesn't send a whole request in this time is dropped, including an idle one.
static constexpr Seconds requestTimeout = 30_s;
static String headerValue(CFHTTPMessageRef message, CFStringRef name)
{
return adoptCF(CFHTTPMessageCopyHeaderFieldValue(message, name)).get();
}
static bool hasVersion(CFHTTPMessageRef message, CFStringRef version)
{
RetainPtr messageVersion = adoptCF(CFHTTPMessageCopyVersion(message));
return messageVersion && CFEqual(messageVersion.get(), version);
}
class HTTPConnection : public RefCountedAndCanMakeWeakPtr<HTTPConnection> {
public:
static Ref<HTTPConnection> create(nw_connection_t connection, HTTPRequestHandler& requestHandler)
{
return adoptRef(*new HTTPConnection(connection, requestHandler));
}
void start();
private:
HTTPConnection(nw_connection_t, HTTPRequestHandler&);
enum class AfterSending : uint8_t { Continue, StartNextRequest, Close };
void startRequest();
void receive();
void didReceiveData(std::span<const uint8_t>);
bool didReceiveHeader();
void didReceiveRequest(std::span<const uint8_t> body);
bool isFromThisMachine() const;
bool shouldCloseAfterResponse() const;
void respond(HTTPRequestHandler::Response&&, bool closeAfterSending);
void respondWithStatus(unsigned statusCode, bool closeAfterSending) { respond({ statusCode, { }, { } }, closeAfterSending); }
void send(CFHTTPMessageRef, UTF8CString body, AfterSending);
void close();
const OSObjectPtr<nw_connection_t> m_connection;
HTTPRequestHandler& m_requestHandler;
RunLoop::Timer m_requestTimer;
RetainPtr<CFHTTPMessageRef> m_request;
size_t m_headerSize { 0 };
std::optional<size_t> m_contentLength;
Vector<uint8_t> m_nextRequestData;
bool m_isHandlingRequest { false };
bool m_clientDidClose { false };
bool m_isClosed { false };
};
HTTPConnection::HTTPConnection(nw_connection_t connection, HTTPRequestHandler& requestHandler)
: m_connection(connection)
, m_requestHandler(requestHandler)
, m_requestTimer(RunLoop::mainSingleton(), "HTTPConnection::requestTimer"_s, this, &HTTPConnection::close)
{
}
void HTTPConnection::start()
{
nw_connection_set_queue(m_connection.get(), mainDispatchQueueSingleton());
nw_connection_start(m_connection.get());
startRequest();
}
void HTTPConnection::startRequest()
{
m_request = adoptCF(CFHTTPMessageCreateEmpty(kCFAllocatorDefault, true));
m_headerSize = 0;
m_contentLength = std::nullopt;
m_isHandlingRequest = false;
// Receiving more data doesn't restart the timer, so that a client can't hold the connection
// by trickling a request.
m_requestTimer.startOneShot(requestTimeout);
// What the client sent after the previous request is the start of this one.
if (auto data = std::exchange(m_nextRequestData, { }); !data.isEmpty()) {
didReceiveData(data.span());
if (m_isHandlingRequest || m_isClosed)
return;
}
if (m_clientDidClose) {
close();
return;
}
receive();
}
void HTTPConnection::receive()
{
RefPtr protectedThis = this;
nw_connection_receive(m_connection.get(), 1, std::numeric_limits<uint32_t>::max(), ^(dispatch_data_t content, nw_content_context_t, bool isComplete, nw_error_t error) {
if (protectedThis->m_isClosed)
return;
if (error) {
protectedThis->close();
return;
}
protectedThis->m_clientDidClose = isComplete;
if (content) {
const void* bytes = nullptr;
size_t size = 0;
OSObjectPtr contiguousContent = adoptOSObject(dispatch_data_create_map(content, &bytes, &size));
protectedThis->didReceiveData(unsafeMakeSpan(static_cast<const uint8_t*>(bytes), size));
}
// Receiving resumes once the response has been sent.
if (protectedThis->m_isClosed || protectedThis->m_isHandlingRequest)
return;
// The client went away before sending a whole request.
if (protectedThis->m_clientDidClose) {
protectedThis->close();
return;
}
protectedThis->receive();
});
}
void HTTPConnection::didReceiveData(std::span<const uint8_t> data)
{
if (!CFHTTPMessageAppendBytes(m_request.get(), data.data(), data.size())) {
respondWithStatus(400, true);
return;
}
if (!m_contentLength) {
if (!CFHTTPMessageIsHeaderComplete(m_request.get())) {
m_headerSize += data.size();
if (m_headerSize > maximumHeaderSize)
close();
return;
}
if (!didReceiveHeader())
return;
}
// Everything after the header is in the body, including the start of any next request.
RetainPtr body = adoptCF(CFHTTPMessageCopyBody(m_request.get()));
auto bodyBytes = body ? span(body.get()) : std::span<const uint8_t> { };
if (bodyBytes.size() < *m_contentLength)
return;
didReceiveRequest(bodyBytes);
}
bool HTTPConnection::didReceiveHeader()
{
// Only serve clients on this machine, so that web pages can't create sessions. This is checked
// before the body is read, so that a page can't make this side buffer one.
if (!isFromThisMachine()) {
auto error = JSON::Object::create();
error->setString("error"_s, "invalid argument"_s);
error->setString("message"_s, "'Host' header or 'Origin' header is specified and is not localhost."_s);
error->setString("stacktrace"_s, emptyString());
auto responseBody = JSON::Object::create();
responseBody->setObject("value"_s, WTF::move(error));
respond({ 400, responseBody->toJSONString().utf8(), "application/json; charset=utf-8"_s }, true);
return false;
}
// A request line that CFHTTPMessage can't make sense of has no method.
if (RetainPtr method = adoptCF(CFHTTPMessageCopyRequestMethod(m_request.get())); !method) {
respondWithStatus(405, true);
return false;
}
// WebDriver clients send bodies with a Content-Length, so chunked bodies aren't supported.
if (!headerValue(m_request.get(), CFSTR("Transfer-Encoding")).isNull()) {
respondWithStatus(501, true);
return false;
}
auto contentLength = headerValue(m_request.get(), CFSTR("Content-Length"));
if (contentLength.isNull())
m_contentLength = 0;
else {
m_contentLength = parseInteger<size_t>(contentLength);
if (!m_contentLength) {
respondWithStatus(400, true);
return false;
}
if (*m_contentLength > maximumBodySize) {
respondWithStatus(413, true);
return false;
}
}
if (!contentLength.isNull() && hasVersion(m_request.get(), kCFHTTPVersion1_1)) {
auto expect = headerValue(m_request.get(), CFSTR("Expect"));
for (auto token : StringView { expect }.split(',')) {
if (equalLettersIgnoringASCIICase(token.trim(isASCIIWhitespace<char16_t>), "100-continue"_s)) {
send(adoptCF(CFHTTPMessageCreateResponse(kCFAllocatorDefault, 100, nullptr, kCFHTTPVersion1_1)).get(), { }, AfterSending::Continue);
break;
}
}
}
return true;
}
bool HTTPConnection::isFromThisMachine() const
{
auto host = headerValue(m_request.get(), CFSTR("Host"));
if (!host.isNull() && !isLoopbackURL(makeString("http://"_s, host)))
return false;
auto origin = headerValue(m_request.get(), CFSTR("Origin"));
return origin.isNull() || isLoopbackURL(origin);
}
void HTTPConnection::didReceiveRequest(std::span<const uint8_t> body)
{
// Handling a request can take much longer than receiving it.
m_requestTimer.stop();
m_nextRequestData = body.subspan(*m_contentLength);
body = body.first(*m_contentLength);
if (!hasVersion(m_request.get(), kCFHTTPVersion1_0) && !hasVersion(m_request.get(), kCFHTTPVersion1_1)) {
respondWithStatus(505, shouldCloseAfterResponse());
return;
}
RetainPtr method = adoptCF(CFHTTPMessageCopyRequestMethod(m_request.get()));
RetainPtr url = adoptCF(CFHTTPMessageCopyRequestURL(m_request.get()));
if (!url) {
respondWithStatus(400, true);
return;
}
RetainPtr path = adoptCF(CFURLCopyPath(url.get()));
RetainPtr decodedPath = adoptCF(CFURLCreateStringByReplacingPercentEscapes(kCFAllocatorDefault, path.get(), CFSTR("")));
HTTPRequestHandler::Request request;
request.method = method.get();
request.path = decodedPath ? decodedPath.get() : path.get();
if (!body.empty()) {
request.data = byteCast<char>(body.data());
request.dataLength = body.size();
}
// The request handler reads the body before it returns.
m_isHandlingRequest = true;
m_requestHandler.handleRequest(WTF::move(request), [protectedThis = Ref { *this }, closeAfterSending = shouldCloseAfterResponse()](HTTPRequestHandler::Response&& response) {
protectedThis->respond(WTF::move(response), closeAfterSending);
});
}
bool HTTPConnection::shouldCloseAfterResponse() const
{
auto connection = headerValue(m_request.get(), CFSTR("Connection"));
if (hasVersion(m_request.get(), kCFHTTPVersion1_0))
return !equalLettersIgnoringASCIICase(connection, "keep-alive"_s);
return equalLettersIgnoringASCIICase(connection, "close"_s);
}
void HTTPConnection::respond(HTTPRequestHandler::Response&& response, bool closeAfterSending)
{
if (m_isClosed)
return;
// Don't read the next request until this response has been sent.
m_isHandlingRequest = true;
RetainPtr message = adoptCF(CFHTTPMessageCreateResponse(kCFAllocatorDefault, response.statusCode, nullptr, kCFHTTPVersion1_1));
if (!response.contentType.isEmpty())
CFHTTPMessageSetHeaderFieldValue(message.get(), CFSTR("Content-Type"), response.contentType.createCFString().get());
CFHTTPMessageSetHeaderFieldValue(message.get(), CFSTR("Content-Length"), String::number(response.data.length()).createCFString().get());
CFHTTPMessageSetHeaderFieldValue(message.get(), CFSTR("Cache-Control"), CFSTR("no-cache"));
CFHTTPMessageSetHeaderFieldValue(message.get(), CFSTR("Connection"), closeAfterSending ? CFSTR("close") : CFSTR("keep-alive"));
send(message.get(), WTF::move(response.data), closeAfterSending ? AfterSending::Close : AfterSending::StartNextRequest);
}
void HTTPConnection::send(CFHTTPMessageRef message, UTF8CString body, AfterSending afterSending)
{
RetainPtr header = adoptCF(CFHTTPMessageCopySerializedMessage(message));
if (!header) {
close();
return;
}
// Send the body without copying it. The blocks keep the header and body alive.
auto headerBytes = span(header.get());
OSObjectPtr data = adoptOSObject(dispatch_data_create(headerBytes.data(), headerBytes.size(), mainDispatchQueueSingleton(), ^{
(void)header;
}));
if (body.length()) {
auto bodyBytes = body.span();
OSObjectPtr bodyData = adoptOSObject(dispatch_data_create(bodyBytes.data(), bodyBytes.size(), mainDispatchQueueSingleton(), ^{
(void)body;
}));
data = adoptOSObject(dispatch_data_create_concat(data.get(), bodyData.get()));
}
RefPtr protectedThis = this;
nw_connection_send(m_connection.get(), data.get(), NW_CONNECTION_DEFAULT_MESSAGE_CONTEXT, true, ^(nw_error_t error) {
if (error) {
RELEASE_LOG_ERROR(WebDriverClassic, "Sending an HTTP response failed with error %d", nw_error_get_error_code(error));
protectedThis->close();
return;
}
switch (afterSending) {
case AfterSending::Continue:
return;
case AfterSending::StartNextRequest:
protectedThis->startRequest();
return;
case AfterSending::Close:
protectedThis->close();
return;
}
});
}
void HTTPConnection::close()
{
if (m_isClosed)
return;
m_isClosed = true;
m_requestTimer.stop();
nw_connection_cancel(m_connection.get());
}
bool HTTPServer::listen(const std::optional<String>& host, unsigned port)
{
if (!validateListeningHost(host.value_or(String())))
return false;
auto boundPort = startListener("127.0.0.1"_s, port);
if (!boundPort || !startListener("::1"_s, *boundPort)) {
disconnect();
return false;
}
// The WebSocket server only listens on 127.0.0.1, so that's the host in webSocketUrl.
m_visibleHost = "127.0.0.1"_s;
return true;
}
std::optional<uint16_t> HTTPServer::startListener(ASCIILiteral address, uint16_t port)
{
OSObjectPtr parameters = adoptOSObject(nw_parameters_create_secure_tcp(NW_PARAMETERS_DISABLE_PROTOCOL, NW_PARAMETERS_DEFAULT_CONFIGURATION));
nw_parameters_set_reuse_local_address(parameters.get(), true);
nw_parameters_set_local_endpoint(parameters.get(), adoptOSObject(nw_endpoint_create_host(address.characters(), String::number(port).utf8().legacyCStringPointer())).get());
OSObjectPtr listener = adoptOSObject(nw_listener_create(parameters.get()));
if (!listener)
return std::nullopt;
auto& requestHandler = m_requestHandler;
nw_listener_set_new_connection_handler(listener.get(), ^(nw_connection_t connection) {
OSObjectPtr<nw_connection_t> newConnection = connection;
dispatch_async(mainDispatchQueueSingleton(), ^{
HTTPConnection::create(newConnection.get(), requestHandler)->start();
});
});
// The listener's handlers run on a queue of their own, so that this can wait for the listener to
// be ready before the run loop runs.
OSObjectPtr queue = adoptOSObject(dispatch_queue_create("org.webkit.minidriver.HTTPServer", serialQueueWithAutoreleasePoolAttrSingleton()));
nw_listener_set_queue(listener.get(), queue.get());
OSObjectPtr semaphore = adoptOSObject(dispatch_semaphore_create(0));
__block bool isReady = false;
__block bool didReport = false;
nw_listener_set_state_changed_handler(listener.get(), ^(nw_listener_state_t state, nw_error_t error) {
if (didReport) {
if (state == nw_listener_state_failed)
RELEASE_LOG_ERROR(WebDriverClassic, "HTTP listener failed with error %d", nw_error_get_error_code(error));
return;
}
if (state != nw_listener_state_ready && state != nw_listener_state_failed && state != nw_listener_state_waiting && state != nw_listener_state_cancelled)
return;
isReady = state == nw_listener_state_ready;
didReport = true;
dispatch_semaphore_signal(semaphore.get());
});
nw_listener_start(listener.get());
dispatch_semaphore_wait(semaphore.get(), DISPATCH_TIME_FOREVER);
if (!isReady) {
nw_listener_cancel(listener.get());
return std::nullopt;
}
uint16_t boundPort = nw_listener_get_port(listener.get());
m_listeners.append(WTF::move(listener));
return boundPort;
}
void HTTPServer::disconnect()
{
for (auto& listener : m_listeners)
nw_listener_cancel(listener.get());
m_listeners.clear();
}
} // namespace WebDriver
#endif // PLATFORM(COCOA)