blob: 2189e717e27ad7180c30985ccc2e9d4826c47d8a [file] [log] [blame]
/*
* Copyright (C) 2021 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.
*/
#import "config.h"
#import "HTTPServer.h"
#import "PlatformUtilities.h"
#import "TestUIDelegate.h"
#import <WebKit/WKWebViewConfigurationPrivate.h>
#import <WebKit/WKWebsiteDataStorePrivate.h>
#import <wtf/RetainPtr.h>
#import <wtf/WeakObjCPtr.h>
namespace TestWebKitAPI {
TEST(WebSocket, LongMessageNoDeflate)
{
// https://datatracker.ietf.org/doc/html/rfc6455#section-5.2
constexpr size_t headerSizeWithLargePayloadLength = 10;
constexpr size_t maskingKeySize = 4;
constexpr uint8_t payloadLengthIndicatingLargeExtendedPayloadLength = 127;
constexpr uint8_t fin = 0x80;
constexpr uint8_t textFrame = 0x1;
constexpr uint64_t twoMegabytes = 1024 * 1024 * 2;
constexpr size_t expectedReceiveSize = twoMegabytes + headerSizeWithLargePayloadLength + maskingKeySize;
HTTPServer server([=](Connection connection) {
connection.webSocketHandshake([=] {
connection.receiveBytes([=] (Vector<uint8_t>&& received) {
constexpr size_t headerSize = headerSizeWithLargePayloadLength + maskingKeySize;
EXPECT_EQ(expectedReceiveSize, received.size());
std::array<uint8_t, headerSizeWithLargePayloadLength> expectedHeader { 0x81, 0xff, 0x0, 0x0, 0x0, 0x0, 0x0, 0x20, 0x0, 0x0, };
for (size_t i = 0 ; i < headerSizeWithLargePayloadLength; i++)
EXPECT_EQ(received[i], expectedHeader[i]);
std::array<uint8_t, maskingKeySize> mask { received[10], received[11], received[12], received[13] };
for (size_t i = headerSize; i < expectedReceiveSize; i++)
EXPECT_EQ(received[i] ^ mask[(i - headerSize) % maskingKeySize], 'x');
Vector<uint8_t> bytesToSend;
bytesToSend.reserveInitialCapacity(twoMegabytes + headerSizeWithLargePayloadLength);
bytesToSend.uncheckedAppend(fin | textFrame);
bytesToSend.uncheckedAppend(payloadLengthIndicatingLargeExtendedPayloadLength);
for (size_t i = 0; i < 8; i++)
bytesToSend.uncheckedAppend((twoMegabytes >> (8 * (7 - i))) & 0xFF);
for (size_t i = 0; i < twoMegabytes; i++)
bytesToSend.uncheckedAppend('x');
connection.send(WTFMove(bytesToSend));
}, expectedReceiveSize);
});
});
NSString *html = [NSString stringWithFormat:@""
"<script>"
" let twoMegabytes = 2*1024*1024;"
" var ws = new WebSocket('ws://127.0.0.1:%d/');"
" ws.onopen = function() { this.send('x'.repeat(twoMegabytes)); };"
" ws.onmessage = function(msg) { alert(msg.data.length == twoMegabytes ? 'PASS' : 'FAIL - wrong receive length'); };"
" ws.onerror = function(error) { alert('FAIL - error ' + error.message); }"
"</script>", server.port()];
auto webView = adoptNS([WKWebView new]);
[webView loadHTMLString:html baseURL:nil];
EXPECT_WK_STREQ([webView _test_waitForAlert], "PASS");
}
TEST(WebSocket, PageWithAttributedBundleIdentifierDestroyed)
{
HTTPServer server([](Connection connection) {
connection.webSocketHandshake();
});
NSString *html = [NSString stringWithFormat:@""
"<script>"
" var ws = new WebSocket('ws://127.0.0.1:%d/');"
" ws.onopen = function() { alert('opened successfully'); };"
" ws.onerror = function(error) { alert('FAIL - error ' + error.message); }"
"</script>", server.port()];
pid_t originalNetworkProcessPID { 0 };
auto configuration = adoptNS([WKWebViewConfiguration new]);
__block size_t webSocketsConnected { 0 };
auto delegate = adoptNS([TestUIDelegate new]);
delegate.get().runJavaScriptAlertPanelWithMessage = ^(WKWebView *, NSString *message, WKFrameInfo *, void (^completionHandler)(void)) {
EXPECT_WK_STREQ(message, "opened successfully");
webSocketsConnected++;
completionHandler();
};
constexpr size_t viewCount = 20;
@autoreleasepool {
std::array<RetainPtr<WKWebView>, viewCount> views;
for (size_t i = 0; i < viewCount; i++) {
configuration.get()._attributedBundleIdentifier = [NSString stringWithFormat:@"test.bundle.identifier.%zu", i];
views[i] = adoptNS([[WKWebView alloc] initWithFrame:CGRectMake(0, 0, 100, 100) configuration:configuration.get()]);
views[i].get().UIDelegate = delegate.get();
[views[i] loadHTMLString:html baseURL:nil];
}
while (webSocketsConnected < viewCount)
Util::spinRunLoop();
originalNetworkProcessPID = configuration.get().websiteDataStore._networkProcessIdentifier;
}
Util::spinRunLoop(viewCount * 3);
EXPECT_EQ(originalNetworkProcessPID, configuration.get().websiteDataStore._networkProcessIdentifier);
}
#if HAVE(NSURLSESSION_WEBSOCKET)
TEST(WebSocket, CloseCode)
{
bool receivedWebSocketClose { false };
Vector<uint8_t> closeData;
HTTPServer webSocketServer([&](Connection connection) {
connection.webSocketHandshake([&, connection] {
connection.receiveBytes([&, connection](Vector<uint8_t>&& v) {
// https://datatracker.ietf.org/doc/html/rfc6455#section-5.2
constexpr uint8_t connectionCloseOpcode { 0x08 };
constexpr uint8_t opcodeMask { 0x08 };
constexpr uint8_t maskMask { 0x80 };
constexpr uint8_t payloadLengthMask { 0x7F };
constexpr uint8_t headerSize { 6 };
ASSERT_GT(v.size(), 1u);
EXPECT_EQ(v[0] & opcodeMask, connectionCloseOpcode);
EXPECT_TRUE(v[1] & maskMask);
size_t length = v[1] & payloadLengthMask;
ASSERT_EQ(length, v.size() - headerSize);
std::array<uint8_t, 4> mask { v[2], v[3], v[4], v[5] };
for (size_t i = headerSize; i < v.size(); i++)
closeData.append(v[i] ^ mask[(i - headerSize) % 4]);
receivedWebSocketClose = true;
connection.receiveBytes([](Vector<uint8_t>&& v) {
EXPECT_EQ(v.size(), 0u);
});
});
});
});
auto htmlWithOnOpen = [&] (const char* onopen) {
return [NSString stringWithFormat:@""
"<script>"
" var ws = new WebSocket('ws://127.0.0.1:%d/');"
" ws.onopen = function() { %s };"
"</script>", webSocketServer.port(), onopen];
};
HTTPServer httpServer({
{ "/navigateAway"_s, { htmlWithOnOpen("window.location = '/navigationTarget'") }},
{ "/navigationTarget"_s, { "hi"_s } },
{ "/closeCustomCode"_s, { htmlWithOnOpen("ws.close(3000)") } },
{ "/closeNoArguments"_s, { htmlWithOnOpen("ws.close()") } },
{ "/closeBothParameters"_s, { htmlWithOnOpen("ws.close(3001, 'custom reason')") } },
});
auto appendString = [] (Vector<uint8_t>& vector, const char* string) {
auto length = strlen(string);
for (size_t i = 0; i < length; i++)
vector.append(string[i]);
};
auto webView = adoptNS([WKWebView new]);
[webView loadRequest:httpServer.request("/navigateAway"_s)];
Util::run(&receivedWebSocketClose);
Vector<uint8_t> expected { 0x3, 0xe9 }; // NSURLSessionWebSocketCloseCodeGoingAway
appendString(expected, "WebSocket is closed due to suspension.");
EXPECT_EQ(closeData, expected);
receivedWebSocketClose = false;
closeData = { };
expected = { 0xb, 0xb8 }; // 3000
[webView loadRequest:httpServer.request("/closeCustomCode"_s)];
Util::run(&receivedWebSocketClose);
EXPECT_EQ(closeData, expected);
receivedWebSocketClose = false;
closeData = { };
expected = { };
[webView loadRequest:httpServer.request("/closeNoArguments"_s)];
Util::run(&receivedWebSocketClose);
EXPECT_EQ(closeData, expected);
receivedWebSocketClose = false;
closeData = { };
expected = { 0xb, 0xb9 }; // 3001
appendString(expected, "custom reason");
[webView loadRequest:httpServer.request("/closeBothParameters"_s)];
Util::run(&receivedWebSocketClose);
EXPECT_EQ(closeData, expected);
}
#endif // HAVE(NSURLSESSION_WEBSOCKET)
TEST(WebSocket, BlockedWithSubresources)
{
HTTPServer server([](Connection connection) {
connection.webSocketHandshake();
});
NSString *html = [NSString stringWithFormat:@""
"<script>"
" var ws = new WebSocket('ws://127.0.0.1:%d/');"
" ws.onopen = function() { alert('opened successfully'); };"
" ws.onerror = function(error) { alert('FAIL - error ' + error.message); }"
"</script>", server.port()];
{
auto configuration = adoptNS([WKWebViewConfiguration new]);
configuration.get()._loadsSubresources = NO;
auto webView = adoptNS([[WKWebView alloc] initWithFrame:CGRectZero configuration:configuration.get()]);
[webView loadHTMLString:html baseURL:nil];
EXPECT_WK_STREQ([webView _test_waitForAlert], "FAIL - error undefined");
}
auto webView = adoptNS([WKWebView new]);
[webView loadHTMLString:html baseURL:nil];
EXPECT_WK_STREQ([webView _test_waitForAlert], "opened successfully");
}
}