blob: 26bf35bec36e969551769d12c8ef2e1de3c002e4 [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <string_view>
#include <type_traits>
#include "base/check_deref.h"
#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/json/json_writer.h"
#include "base/strings/stringprintf.h"
#include "base/test/scoped_feature_list.h"
#include "build/build_config.h"
#include "chrome/browser/content_settings/host_content_settings_map_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/web_applications/test/isolated_web_app_test_utils.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_url_info.h"
#include "chrome/browser/web_applications/isolated_web_apps/test/isolated_web_app_builder.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/content_settings/core/browser/host_content_settings_map.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_base.h"
#include "content/public/test/browser_test_utils.h"
#include "extensions/browser/api/sockets_udp/test_udp_echo_server.h"
#include "extensions/browser/extension_host.h"
#include "extensions/browser/process_manager.h"
#include "extensions/buildflags/buildflags.h"
#include "extensions/common/manifest_constants.h"
#include "net/base/host_port_pair.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "services/network/public/mojom/network_context.mojom.h"
#include "services/network/public/mojom/permissions_policy/permissions_policy_feature.mojom-shared.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/features_generated.h"
#if BUILDFLAG(ENABLE_EXTENSIONS)
#include "chrome/browser/extensions/extension_apitest.h"
#include "extensions/browser/api/sockets_udp/test_udp_echo_server.h"
#include "extensions/common/extension.h"
#include "extensions/test/extension_test_message_listener.h"
#include "extensions/test/result_catcher.h"
#include "extensions/test/test_extension_dir.h"
#endif // BUILDFLAG(ENABLE_EXTENSIONS)
#if BUILDFLAG(IS_MAC)
#include "base/mac/mac_util.h"
#endif
namespace {
constexpr char kHostname[] = "direct-sockets.com";
constexpr char kPrivateAddress[] = "10.8.0.1";
constexpr std::string_view kTcpReadWriteScript = R"(
new Promise(async (resolve, reject) => {
try {
const socket = new TCPSocket($1, $2);
const { readable, writable } = await socket.opened;
const reader = readable.getReader();
const writer = writable.getWriter();
const kTcpPacket =
"POST /echo HTTP/1.1\r\n" +
"Content-Length: 19\r\n\r\n" +
"0100000005320000005";
// The echo server can send back the response in multiple chunks.
// We must wait for at least `kTcpMinExpectedResponseLength` bytes to
// be received before matching the response with `kTcpResponsePattern`.
const kTcpMinExpectedResponseLength = 102;
const kTcpResponsePattern = "0100000005320000005";
let tcpResponse = "";
const readUntil = async () => {
const { value, done } = await reader.read();
if (done) {
reject("ReadableStream must not be exhausted at this point.");
}
const message = (new TextDecoder()).decode(value);
tcpResponse += message;
if (tcpResponse.length >= kTcpMinExpectedResponseLength) {
if (!tcpResponse.match(kTcpResponsePattern)) {
reject("The data returned must match the data sent.");
}
resolve();
} else {
readUntil();
}
};
writer.write((new TextEncoder()).encode(kTcpPacket));
readUntil();
} catch (err) {
reject(err);
}
});
)";
constexpr std::string_view kUdpConnectedReadWriteScript = R"(
new Promise(async (resolve, reject) => {
try {
const socket = new UDPSocket({ remoteAddress: $1, remotePort: $2 });
const { readable, writable } = await socket.opened;
const kUdpMessage = "udp_message";
writable.getWriter().write({
data: (new TextEncoder()).encode(kUdpMessage)
});
return await readable.getReader().read().then(packet => {
const { value, done } = packet;
if (done) {
reject("ReadableStream must not be exhausted at this point.");
}
const { data } = value;
if ((new TextDecoder()).decode(data) !== kUdpMessage) {
reject("The data returned must match the data sent.");
}
resolve();
});
} catch (err) {
reject(err);
}
});
)";
constexpr std::string_view kUdpBoundReadWriteScript = R"(
new Promise(async (resolve, reject) => {
try {
const socket = new UDPSocket({ localAddress: "127.0.0.1" });
const { readable, writable } = await socket.opened;
const kUdpMessage = "udp_message";
writable.getWriter().write({
data: (new TextEncoder()).encode(kUdpMessage),
remoteAddress: $1,
remotePort: $2,
});
return await readable.getReader().read().then(packet => {
const { value, done } = packet;
if (done) {
reject("ReadableStream must not be exhausted at this point.");
}
const { data, remoteAddress, remotePort } = value;
if ((new TextDecoder()).decode(data) !== kUdpMessage) {
reject("The data returned must match the data sent.");
}
if (remoteAddress !== "127.0.0.1") {
reject(`Expected remoteAddress = 127.0.0.1, got ${remoteAddress}`);
}
if (remotePort !== $2) {
reject(`Expected remotePort = $2, got ${remotePort}`);
}
resolve();
});
} catch (err) {
reject(err);
}
});
)";
static constexpr std::string_view kTcpServerExchangePacketWithTcpScript = R"(
new Promise(async (resolve, reject) => {
const assertEq = (actual, expected) => {
const jf = e => JSON.stringify(e);
if (actual !== expected) {
reject(`Expected ${jf(expected)}, got ${jf(actual)}`);
}
};
const kPacket = "I'm a netcat. Meow-meow!";
// |localPort| is intentionally omitted so that the OS will pick one itself.
const serverSocket = new TCPServerSocket('127.0.0.1');
const { localPort: serverSocketPort } = await serverSocket.opened;
// Connect a client to the server.
const clientSocket = new TCPSocket('127.0.0.1', serverSocketPort);
async function acceptOnce() {
const { readable } = await serverSocket.opened;
const reader = readable.getReader();
const { value: acceptedSocket, done } = await reader.read();
assertEq(done, false);
reader.releaseLock();
return acceptedSocket;
};
const acceptedSocket = await acceptOnce();
await clientSocket.opened;
const encoder = new TextEncoder();
const decoder = new TextDecoder();
async function acceptedSocketSend() {
const { writable } = await acceptedSocket.opened;
const writer = writable.getWriter();
await writer.ready;
await writer.write(encoder.encode(kPacket));
writer.releaseLock();
}
async function clientSocketReceive() {
const { readable } = await clientSocket.opened;
const reader = readable.getReader();
let result = "";
while (result.length < kPacket.length) {
const { value, done } = await reader.read();
assertEq(done, false);
result += decoder.decode(value);
}
reader.releaseLock();
assertEq(result, kPacket);
}
acceptedSocketSend();
await clientSocketReceive();
await clientSocket.close();
await acceptedSocket.close();
await serverSocket.close();
resolve();
});
)";
#if BUILDFLAG(ENABLE_EXTENSIONS)
base::Value::Dict GenerateManifest(
std::optional<base::Value::Dict> socket_permissions = {}) {
auto manifest = base::Value::Dict()
.Set(extensions::manifest_keys::kName,
"Direct Sockets in Chrome Apps")
.Set(extensions::manifest_keys::kManifestVersion, 2)
.Set(extensions::manifest_keys::kVersion, "1.0");
manifest.SetByDottedPath(
extensions::manifest_keys::kPlatformAppBackgroundScripts,
base::Value::List().Append("background.js"));
if (socket_permissions) {
manifest.Set(extensions::manifest_keys::kSockets,
std::move(*socket_permissions));
}
return manifest;
}
auto AccessBlocked() {
return testing::HasSubstr("Access to the requested host or port is blocked");
}
auto PrivateNetworkAccessBlocked() {
return testing::HasSubstr("Access to private network is blocked");
}
auto ErrorIs(const auto& matcher) {
return content::EvalJsResult::ErrorIs(matcher);
}
auto IsOk() {
return content::EvalJsResult::IsOk();
}
#endif
class TestServer {
public:
virtual ~TestServer() = default;
virtual void Start(network::mojom::NetworkContext* network_context) = 0;
virtual void Stop() = 0;
virtual uint16_t port() const = 0;
};
class TcpHttpTestServer : public TestServer {
public:
void Start(network::mojom::NetworkContext* network_context) override {
DCHECK(!test_server_);
test_server_ = std::make_unique<net::EmbeddedTestServer>(
net::EmbeddedTestServer::TYPE_HTTP);
test_server_->AddDefaultHandlers();
ASSERT_TRUE(test_server_->Start());
}
void Stop() override { test_server_.reset(); }
uint16_t port() const override {
DCHECK(test_server_);
return test_server_->port();
}
private:
std::unique_ptr<net::EmbeddedTestServer> test_server_;
};
class UdpEchoTestServer : public TestServer {
public:
void Start(network::mojom::NetworkContext* network_context) override {
DCHECK(!udp_echo_server_);
udp_echo_server_ = std::make_unique<extensions::TestUdpEchoServer>();
net::HostPortPair host_port_pair;
ASSERT_TRUE(udp_echo_server_->Start(network_context, &host_port_pair));
port_ = host_port_pair.port();
ASSERT_GT(*port_, 0);
}
void Stop() override { udp_echo_server_.reset(); }
uint16_t port() const override {
DCHECK(port_);
return *port_;
}
private:
std::unique_ptr<extensions::TestUdpEchoServer> udp_echo_server_;
std::optional<uint16_t> port_;
};
template <typename TestHarness>
requires(std::is_base_of_v<InProcessBrowserTest, TestHarness>)
class ChromeDirectSocketsTest : public TestHarness {
public:
ChromeDirectSocketsTest() = delete;
void SetUpOnMainThread() override {
TestHarness::SetUpOnMainThread();
TestHarness::host_resolver()->AddRule(kHostname, "127.0.0.1");
test_server()->Start(InProcessBrowserTest::browser()
->profile()
->GetDefaultStoragePartition()
->GetNetworkContext());
}
void TearDownOnMainThread() override {
TestHarness::TearDownOnMainThread();
test_server()->Stop();
}
protected:
explicit ChromeDirectSocketsTest(std::unique_ptr<TestServer> test_server)
: test_server_{std::move(test_server)} {}
TestServer* test_server() const {
DCHECK(test_server_);
return test_server_.get();
}
private:
std::unique_ptr<TestServer> test_server_;
};
template <typename TestHarness>
class ChromeDirectSocketsTcpTest : public ChromeDirectSocketsTest<TestHarness> {
public:
ChromeDirectSocketsTcpTest()
: ChromeDirectSocketsTest<TestHarness>{
std::make_unique<TcpHttpTestServer>()} {}
};
template <typename TestHarness>
class ChromeDirectSocketsUdpTest : public ChromeDirectSocketsTest<TestHarness> {
public:
ChromeDirectSocketsUdpTest()
: ChromeDirectSocketsTest<TestHarness>{
std::make_unique<UdpEchoTestServer>()} {}
};
#if BUILDFLAG(ENABLE_EXTENSIONS)
class ChromeAppApiTest : public extensions::ExtensionApiTest {
public:
static constexpr std::string_view kWorkerScriptTemplate = R"(
self.onmessage = async e => {
try {
await %s;
self.postMessage(null);
} catch (err) {
self.postMessage({ error: err });
}
};
)";
static constexpr std::string_view kWorkerConnect = R"(
new Promise((resolve, reject) => {
const policy = trustedTypes.createPolicy("default", {
createScriptURL: (url) => url,
});
const worker = new Worker(
policy.createScriptURL('/worker.js')
);
worker.onmessage = e => {
if (e.data) {
reject(e.data.error);
} else {
resolve();
}
};
worker.postMessage(null);
});
)";
content::RenderFrameHost* InstallAndOpenChromeApp(
const base::Value::Dict& manifest) {
dir_.WriteManifest(manifest);
dir_.WriteFile(FILE_PATH_LITERAL("background.js"), "");
return InstallAndOpenChromeApp();
}
content::RenderFrameHost* InstallAndOpenChromeAppWithWorkerScript(
const base::Value::Dict& manifest,
std::string_view worker_script) {
dir_.WriteManifest(manifest);
dir_.WriteFile(FILE_PATH_LITERAL("background.js"), "");
dir_.WriteFile(FILE_PATH_LITERAL("worker.js"), worker_script);
return InstallAndOpenChromeApp();
}
private:
content::RenderFrameHost* InstallAndOpenChromeApp() {
const extensions::Extension& extension =
CHECK_DEREF(LoadExtension(dir_.UnpackedPath()));
return CHECK_DEREF(extensions::ProcessManager::Get(profile())
->GetBackgroundHostForExtension(extension.id()))
.main_frame_host();
}
extensions::TestExtensionDir dir_;
};
using ChromeDirectSocketsTcpApiTest =
ChromeDirectSocketsTcpTest<ChromeAppApiTest>;
IN_PROC_BROWSER_TEST_F(ChromeDirectSocketsTcpApiTest, TcpReadWrite) {
content::RenderFrameHost* app_frame = InstallAndOpenChromeApp(
GenerateManifest(/*socket_permissions=*/
base::Value::Dict().Set(
"tcp", base::Value::Dict().Set("connect", "*"))));
ASSERT_THAT(
EvalJs(app_frame, content::JsReplace(kTcpReadWriteScript, kHostname,
test_server()->port())),
IsOk());
}
IN_PROC_BROWSER_TEST_F(ChromeDirectSocketsTcpApiTest, TcpReadWriteFromWorker) {
const std::string worker_script = base::StringPrintf(
kWorkerScriptTemplate, content::JsReplace(kTcpReadWriteScript, kHostname,
test_server()->port()));
content::RenderFrameHost* app_frame = InstallAndOpenChromeAppWithWorkerScript(
GenerateManifest(/*socket_permissions=*/base::Value::Dict().Set(
"tcp", base::Value::Dict().Set("connect", "*"))),
worker_script);
ASSERT_THAT(EvalJs(app_frame, kWorkerConnect), IsOk());
}
IN_PROC_BROWSER_TEST_F(ChromeDirectSocketsTcpApiTest,
TcpSocketUndefinedWithoutSocketsPermission) {
// "sockets" key is not present in the manifest.
content::RenderFrameHost* app_frame =
InstallAndOpenChromeApp(GenerateManifest());
static constexpr std::string_view kScript = R"(
(async () => {
const socket = new TCPSocket($1, $2);
await socket.opened;
})();
)";
EXPECT_THAT(EvalJs(app_frame, content::JsReplace(kScript, kHostname, 0)),
ErrorIs(AccessBlocked()));
}
IN_PROC_BROWSER_TEST_F(ChromeDirectSocketsTcpApiTest,
TcpFailsWithoutSocketsTcpConnectPermission) {
// "sockets" key is present in the manifest, but "sockets.tcp.connect" is not.
content::RenderFrameHost* app_frame = InstallAndOpenChromeApp(
GenerateManifest(/*socket_permissions=*/base::Value::Dict()));
static constexpr std::string_view kScript = R"(
(async () => {
const socket = new TCPSocket($1, $2);
await socket.opened;
})();
)";
EXPECT_THAT(EvalJs(app_frame, content::JsReplace(kScript, kHostname,
test_server()->port())),
ErrorIs(AccessBlocked()));
}
using ChromeDirectSocketsUdpApiTest =
ChromeDirectSocketsUdpTest<ChromeAppApiTest>;
IN_PROC_BROWSER_TEST_F(ChromeDirectSocketsUdpApiTest, UdpReadWrite) {
content::RenderFrameHost* app_frame = InstallAndOpenChromeApp(
GenerateManifest(/*socket_permissions=*/base::Value::Dict().Set(
"udp", base::Value::Dict().Set("send", "*"))));
ASSERT_THAT(
EvalJs(app_frame, content::JsReplace(kUdpConnectedReadWriteScript,
kHostname, test_server()->port())),
IsOk());
}
IN_PROC_BROWSER_TEST_F(ChromeDirectSocketsUdpApiTest, UdpReadWriteFromWorker) {
const std::string worker_script =
base::StringPrintf(kWorkerScriptTemplate,
content::JsReplace(kUdpConnectedReadWriteScript,
kHostname, test_server()->port()));
content::RenderFrameHost* app_frame = InstallAndOpenChromeAppWithWorkerScript(
GenerateManifest(/*socket_permissions=*/base::Value::Dict().Set(
"udp", base::Value::Dict().Set("send", "*"))),
worker_script);
ASSERT_THAT(EvalJs(app_frame, kWorkerConnect), IsOk());
}
IN_PROC_BROWSER_TEST_F(ChromeDirectSocketsUdpApiTest,
UdpSocketUndefinedWithoutSocketsPermission) {
// "sockets" key is not present in the manifest.
content::RenderFrameHost* app_frame =
InstallAndOpenChromeApp(GenerateManifest());
static constexpr std::string_view kScript = R"(
(async () => {
const socket = new UDPSocket({ remoteAddress: $1, remotePort: $2 });
await socket.opened;
})();
)";
EXPECT_THAT(EvalJs(app_frame, content::JsReplace(kScript, kHostname, 0)),
ErrorIs(AccessBlocked()));
}
IN_PROC_BROWSER_TEST_F(ChromeDirectSocketsUdpApiTest,
UdpConnectedFailsWithoutSocketsUdpSendPermission) {
// "sockets" key is present in the manifest, but "sockets.udp.send" is
// not.
content::RenderFrameHost* app_frame = InstallAndOpenChromeApp(
GenerateManifest(/*socket_permissions=*/base::Value::Dict()));
static constexpr std::string_view kScript = R"(
(async () => {
const socket = new UDPSocket({ remoteAddress: $1, remotePort: $2 });
await socket.opened;
})();
)";
EXPECT_THAT(EvalJs(app_frame, content::JsReplace(kScript, kHostname,
test_server()->port())),
ErrorIs(AccessBlocked()));
}
IN_PROC_BROWSER_TEST_F(ChromeDirectSocketsUdpApiTest,
UdpBoundFailsWithoutSocketsUdpBindPermission) {
// "sockets" key is present in the manifest as well as "sockets.udp.send",
// but "sockets.udp.bind" is not.
content::RenderFrameHost* app_frame = InstallAndOpenChromeApp(
GenerateManifest(/*socket_permissions=*/base::Value::Dict()));
static constexpr std::string_view kScript = R"(
(async () => {
const socket = new UDPSocket({ localAddress: "::" });
await socket.opened;
})();
)";
EXPECT_THAT(EvalJs(app_frame, kScript), ErrorIs(AccessBlocked()));
}
IN_PROC_BROWSER_TEST_F(ChromeDirectSocketsUdpApiTest, UdpServerReadWrite) {
content::RenderFrameHost* app_frame = InstallAndOpenChromeApp(
GenerateManifest(/*socket_permissions=*/base::Value::Dict().Set(
"udp", base::Value::Dict().Set("bind", "*").Set("send", "*"))));
ASSERT_THAT(
EvalJs(app_frame, content::JsReplace(kUdpBoundReadWriteScript, kHostname,
test_server()->port())),
IsOk());
}
IN_PROC_BROWSER_TEST_F(ChromeDirectSocketsUdpApiTest,
UdpServerNotAffectedByPNAContentSettingInChromeApps) {
content::RenderFrameHost* app_frame = InstallAndOpenChromeApp(
GenerateManifest(/*socket_permissions=*/base::Value::Dict().Set(
"udp", base::Value::Dict().Set("bind", "*").Set("send", "*"))));
HostContentSettingsMapFactory::GetForProfile(profile())
->SetDefaultContentSetting(
ContentSettingsType::DIRECT_SOCKETS_PRIVATE_NETWORK_ACCESS,
ContentSetting::CONTENT_SETTING_BLOCK);
constexpr std::string_view kUdpBoundPna = R"(
(async () => {
const socket = new UDPSocket({ localAddress: "0.0.0.0" });
await socket.opened;
})();
)";
ASSERT_THAT(EvalJs(app_frame, kUdpBoundPna), IsOk());
}
using ChromeDirectSocketsTcpServerApiTest = ChromeAppApiTest;
IN_PROC_BROWSER_TEST_F(ChromeDirectSocketsTcpServerApiTest,
TcpServerSocketUndefinedWithoutSocketsPermission) {
// "sockets" key is not present in the manifest.
content::RenderFrameHost* app_frame =
InstallAndOpenChromeApp(GenerateManifest());
static constexpr std::string_view kScript = R"(
(async () => {
const socket = new TCPServerSocket("::");
await socket.opened;
})();
)";
EXPECT_THAT(EvalJs(app_frame, kScript), ErrorIs(AccessBlocked()));
}
IN_PROC_BROWSER_TEST_F(ChromeDirectSocketsTcpServerApiTest,
TcpServerFailsWithoutSocketsTcpServerListenPermission) {
// "sockets" key is present in the manifest, but "sockets.tcpServer.listen" is
// not.
content::RenderFrameHost* app_frame = InstallAndOpenChromeApp(
GenerateManifest(/*socket_permissions=*/base::Value::Dict()));
static constexpr std::string_view kScript = R"(
(async () => {
const socket = new TCPServerSocket("::");
await socket.opened;
})();
)";
EXPECT_THAT(EvalJs(app_frame, kScript), ErrorIs(AccessBlocked()));
}
IN_PROC_BROWSER_TEST_F(ChromeDirectSocketsTcpServerApiTest,
TcpServerExchangePacketWithTcpSocket) {
content::RenderFrameHost* app_frame =
InstallAndOpenChromeApp(GenerateManifest(
/*socket_permissions=*/base::Value::Dict()
.Set("tcpServer", base::Value::Dict().Set("listen", "*"))
.Set("tcp", base::Value::Dict().Set("connect", "*"))));
EXPECT_THAT(EvalJs(app_frame, kTcpServerExchangePacketWithTcpScript), IsOk());
}
#endif
class IsolatedWebAppApiTest : public web_app::IsolatedWebAppBrowserTestHarness {
public:
content::RenderFrameHost* InstallAndOpenIsolatedWebApp(
bool with_pna = false) {
using PermissionsPolicyFeature = network::mojom::PermissionsPolicyFeature;
auto manifest_builder =
web_app::ManifestBuilder().AddPermissionsPolicyWildcard(
PermissionsPolicyFeature::kDirectSockets);
if (with_pna) {
manifest_builder.AddPermissionsPolicyWildcard(
PermissionsPolicyFeature::kDirectSocketsPrivate);
}
auto app = web_app::IsolatedWebAppBuilder(std::move(manifest_builder))
.BuildBundle();
web_app::IsolatedWebAppUrlInfo url_info = app->Install(profile()).value();
return OpenApp(url_info.app_id());
}
};
class IsolatedWebAppSharedWorkerApiTest
: public web_app::IsolatedWebAppBrowserTestHarness {
public:
static constexpr std::string_view kSharedWorkerScriptTemplate = R"(
onconnect = async e => {
const port = e.ports[0];
port.start();
try {
await %s;
port.postMessage(null);
} catch (err) {
port.postMessage({ 'error': err });
}
};
)";
static constexpr std::string_view kSharedWorkerConnect = R"(
new Promise((resolve, reject) => {
const policy = trustedTypes.createPolicy("default", {
createScriptURL: (url) => url,
});
const worker = new SharedWorker(
policy.createScriptURL('/shared_worker.js')
);
worker.port.onmessage = e => {
if (e.data) {
reject(e.data.error);
} else {
resolve();
}
};
});
)";
content::RenderFrameHost* InstallAndOpenIsolatedWebAppWithSharedWorkerScript(
std::string_view shared_worker_script,
bool with_pna = false) {
using PermissionsPolicyFeature = network::mojom::PermissionsPolicyFeature;
auto manifest_builder =
web_app::ManifestBuilder().AddPermissionsPolicyWildcard(
PermissionsPolicyFeature::kDirectSockets);
if (with_pna) {
manifest_builder.AddPermissionsPolicyWildcard(
PermissionsPolicyFeature::kDirectSocketsPrivate);
}
auto app = web_app::IsolatedWebAppBuilder(std::move(manifest_builder))
.AddJs("/shared_worker.js", shared_worker_script)
.BuildBundle();
web_app::IsolatedWebAppUrlInfo url_info = app->Install(profile()).value();
return OpenApp(url_info.app_id());
}
private:
base::test::ScopedFeatureList features_{
blink::features::kDirectSocketsInSharedWorkers};
};
class IsolatedWebAppServiceWorkerApiTest
: public web_app::IsolatedWebAppBrowserTestHarness {
public:
static constexpr std::string_view kServiceWorkerScriptTemplate = R"(
addEventListener('message', async (e) => {
try {
await %s;
e.source.postMessage(null);
} catch (err) {
e.source.postMessage({ 'error': err });
}
});
)";
static constexpr std::string_view kServiceWorkerConnect = R"(
new Promise(async (resolve, reject) => {
const policy = trustedTypes.createPolicy("default", {
createScriptURL: (url) => url,
});
await navigator.serviceWorker.register(
policy.createScriptURL('/service_worker.js')
);
navigator.serviceWorker.addEventListener('message', e => {
if (e.data) {
reject(e.data.error);
} else {
resolve();
}
});
const reg = await navigator.serviceWorker.ready;
reg.active.postMessage(null);
});
)";
content::RenderFrameHost* InstallAndOpenIsolatedWebAppWithServiceWorkerScript(
std::string_view service_worker_script,
bool with_pna = false) {
using PermissionsPolicyFeature = network::mojom::PermissionsPolicyFeature;
auto manifest_builder =
web_app::ManifestBuilder().AddPermissionsPolicyWildcard(
PermissionsPolicyFeature::kDirectSockets);
if (with_pna) {
manifest_builder.AddPermissionsPolicyWildcard(
PermissionsPolicyFeature::kDirectSocketsPrivate);
}
auto app = web_app::IsolatedWebAppBuilder(std::move(manifest_builder))
.AddJs("/service_worker.js", service_worker_script)
.BuildBundle();
web_app::IsolatedWebAppUrlInfo url_info = app->Install(profile()).value();
return OpenApp(url_info.app_id());
}
private:
base::test::ScopedFeatureList features_{
blink::features::kDirectSocketsInServiceWorkers};
};
template <typename T>
class ChromeDirectSocketsTcpIsolatedWebAppTestBase
: public ChromeDirectSocketsTcpTest<T> {
void SetUpOnMainThread() override {
#if BUILDFLAG(IS_MAC)
if (base::mac::MacOSMajorVersion() == 13) {
GTEST_SKIP()
<< "Skipping flaky test on MacOS 13, see crbug.com/397993345";
}
#endif // BUILDFLAG(IS_MAC)
ChromeDirectSocketsTcpTest<T>::SetUpOnMainThread();
}
};
using ChromeDirectSocketsTcpIsolatedWebAppTest =
ChromeDirectSocketsTcpIsolatedWebAppTestBase<IsolatedWebAppApiTest>;
using ChromeDirectSocketsTcpIsolatedWebAppSharedWorkerTest =
ChromeDirectSocketsTcpIsolatedWebAppTestBase<
IsolatedWebAppSharedWorkerApiTest>;
using ChromeDirectSocketsTcpIsolatedWebAppServiceWorkerTest =
ChromeDirectSocketsTcpIsolatedWebAppTestBase<
IsolatedWebAppServiceWorkerApiTest>;
IN_PROC_BROWSER_TEST_F(ChromeDirectSocketsTcpIsolatedWebAppTest, TcpReadWrite) {
content::RenderFrameHost* app_frame = InstallAndOpenIsolatedWebApp();
ASSERT_THAT(
EvalJs(app_frame, content::JsReplace(kTcpReadWriteScript, kHostname,
test_server()->port())),
IsOk());
}
IN_PROC_BROWSER_TEST_F(ChromeDirectSocketsTcpIsolatedWebAppSharedWorkerTest,
TcpReadWrite) {
const std::string shared_worker_script =
base::StringPrintf(kSharedWorkerScriptTemplate,
content::JsReplace(kTcpReadWriteScript, kHostname,
test_server()->port()));
content::RenderFrameHost* app_frame =
InstallAndOpenIsolatedWebAppWithSharedWorkerScript(shared_worker_script);
ASSERT_THAT(EvalJs(app_frame, kSharedWorkerConnect), IsOk());
}
IN_PROC_BROWSER_TEST_F(ChromeDirectSocketsTcpIsolatedWebAppServiceWorkerTest,
TcpReadWrite) {
const std::string service_worker_script =
base::StringPrintf(kServiceWorkerScriptTemplate,
content::JsReplace(kTcpReadWriteScript, kHostname,
test_server()->port()));
content::RenderFrameHost* app_frame =
InstallAndOpenIsolatedWebAppWithServiceWorkerScript(
service_worker_script);
ASSERT_THAT(EvalJs(app_frame, kServiceWorkerConnect), IsOk());
}
IN_PROC_BROWSER_TEST_F(ChromeDirectSocketsTcpIsolatedWebAppTest,
TcpConnectionToPrivateFailsWithoutPNAPermission) {
content::RenderFrameHost* app_frame =
InstallAndOpenIsolatedWebApp(/*with_pna=*/false);
constexpr std::string_view kTcpPna = R"(
(async () => {
const socket = new TCPSocket($1, 459);
await socket.opened;
})();
)";
ASSERT_THAT(EvalJs(app_frame, content::JsReplace(kTcpPna, kPrivateAddress)),
ErrorIs(PrivateNetworkAccessBlocked()));
}
IN_PROC_BROWSER_TEST_F(ChromeDirectSocketsTcpIsolatedWebAppTest,
TcpConnectionToPrivateFailsWithoutPNAContentSetting) {
content::RenderFrameHost* app_frame =
InstallAndOpenIsolatedWebApp(/*with_pna=*/true);
HostContentSettingsMapFactory::GetForProfile(profile())
->SetDefaultContentSetting(
ContentSettingsType::DIRECT_SOCKETS_PRIVATE_NETWORK_ACCESS,
ContentSetting::CONTENT_SETTING_BLOCK);
constexpr std::string_view kTcpPna = R"(
(async () => {
const socket = new TCPSocket($1, 459);
await socket.opened;
})();
)";
ASSERT_THAT(EvalJs(app_frame, content::JsReplace(kTcpPna, kPrivateAddress)),
ErrorIs(PrivateNetworkAccessBlocked()));
}
template <typename T>
class ChromeDirectSocketsUdpIsolatedWebAppTestBase
: public ChromeDirectSocketsUdpTest<T> {
void SetUpOnMainThread() override {
#if BUILDFLAG(IS_MAC)
if (base::mac::MacOSMajorVersion() == 13) {
GTEST_SKIP()
<< "Skipping flaky test on MacOS 13, see crbug.com/397993345";
}
#endif // BUILDFLAG(IS_MAC)
ChromeDirectSocketsUdpTest<T>::SetUpOnMainThread();
}
};
using ChromeDirectSocketsUdpIsolatedWebAppTest =
ChromeDirectSocketsUdpIsolatedWebAppTestBase<IsolatedWebAppApiTest>;
using ChromeDirectSocketsUdpIsolatedWebAppSharedWorkerTest =
ChromeDirectSocketsUdpIsolatedWebAppTestBase<
IsolatedWebAppSharedWorkerApiTest>;
using ChromeDirectSocketsUdpIsolatedWebAppServiceWorkerTest =
ChromeDirectSocketsUdpIsolatedWebAppTestBase<
IsolatedWebAppServiceWorkerApiTest>;
IN_PROC_BROWSER_TEST_F(ChromeDirectSocketsUdpIsolatedWebAppTest, UdpReadWrite) {
content::RenderFrameHost* app_frame = InstallAndOpenIsolatedWebApp();
ASSERT_THAT(
EvalJs(app_frame, content::JsReplace(kUdpConnectedReadWriteScript,
kHostname, test_server()->port())),
IsOk());
}
IN_PROC_BROWSER_TEST_F(ChromeDirectSocketsUdpIsolatedWebAppSharedWorkerTest,
UdpReadWrite) {
const std::string shared_worker_script =
base::StringPrintf(kSharedWorkerScriptTemplate,
content::JsReplace(kUdpConnectedReadWriteScript,
kHostname, test_server()->port()));
content::RenderFrameHost* app_frame =
InstallAndOpenIsolatedWebAppWithSharedWorkerScript(shared_worker_script);
ASSERT_THAT(EvalJs(app_frame, kSharedWorkerConnect), IsOk());
}
IN_PROC_BROWSER_TEST_F(ChromeDirectSocketsUdpIsolatedWebAppServiceWorkerTest,
UdpReadWrite) {
const std::string service_worker_script =
base::StringPrintf(kServiceWorkerScriptTemplate,
content::JsReplace(kUdpConnectedReadWriteScript,
kHostname, test_server()->port()));
content::RenderFrameHost* app_frame =
InstallAndOpenIsolatedWebAppWithServiceWorkerScript(
service_worker_script);
ASSERT_THAT(EvalJs(app_frame, kServiceWorkerConnect), IsOk());
}
IN_PROC_BROWSER_TEST_F(ChromeDirectSocketsUdpIsolatedWebAppTest,
UdpConnectionToPrivateFailsWithoutPNAPermission) {
content::RenderFrameHost* app_frame =
InstallAndOpenIsolatedWebApp(/*with_pna=*/false);
constexpr std::string_view kUdpPna = R"(
(async () => {
const socket = new UDPSocket({ remoteAddress: $1, remotePort: 459 });
await socket.opened;
})();
)";
ASSERT_THAT(EvalJs(app_frame, content::JsReplace(kUdpPna, kPrivateAddress)),
ErrorIs(PrivateNetworkAccessBlocked()));
}
IN_PROC_BROWSER_TEST_F(ChromeDirectSocketsUdpIsolatedWebAppTest,
UdpConnectionToPrivateFailsWithoutPNAContentSetting) {
content::RenderFrameHost* app_frame =
InstallAndOpenIsolatedWebApp(/*with_pna=*/true);
HostContentSettingsMapFactory::GetForProfile(profile())
->SetDefaultContentSetting(
ContentSettingsType::DIRECT_SOCKETS_PRIVATE_NETWORK_ACCESS,
ContentSetting::CONTENT_SETTING_BLOCK);
constexpr std::string_view kUdpPna = R"(
(async () => {
const socket = new UDPSocket({ remoteAddress: $1, remotePort: 459 });
await socket.opened;
})();
)";
ASSERT_THAT(EvalJs(app_frame, content::JsReplace(kUdpPna, kPrivateAddress)),
ErrorIs(PrivateNetworkAccessBlocked()));
}
IN_PROC_BROWSER_TEST_F(ChromeDirectSocketsUdpIsolatedWebAppTest,
UdpServerReadWrite) {
// UDP Bound Mode requires direct-sockets-private permissions policy.
content::RenderFrameHost* app_frame =
InstallAndOpenIsolatedWebApp(/*with_pna=*/true);
ASSERT_THAT(
EvalJs(app_frame, content::JsReplace(kUdpBoundReadWriteScript, kHostname,
test_server()->port())),
IsOk());
}
IN_PROC_BROWSER_TEST_F(ChromeDirectSocketsUdpIsolatedWebAppSharedWorkerTest,
UdpServerReadWrite) {
const std::string shared_worker_script =
base::StringPrintf(kSharedWorkerScriptTemplate,
content::JsReplace(kUdpBoundReadWriteScript, kHostname,
test_server()->port()));
content::RenderFrameHost* app_frame =
InstallAndOpenIsolatedWebAppWithSharedWorkerScript(shared_worker_script);
ASSERT_THAT(EvalJs(app_frame, kSharedWorkerConnect), IsOk());
}
IN_PROC_BROWSER_TEST_F(ChromeDirectSocketsUdpIsolatedWebAppServiceWorkerTest,
UdpServerReadWrite) {
const std::string service_worker_script =
base::StringPrintf(kServiceWorkerScriptTemplate,
content::JsReplace(kUdpBoundReadWriteScript, kHostname,
test_server()->port()));
content::RenderFrameHost* app_frame =
InstallAndOpenIsolatedWebAppWithServiceWorkerScript(
service_worker_script);
ASSERT_THAT(EvalJs(app_frame, kServiceWorkerConnect), IsOk());
}
IN_PROC_BROWSER_TEST_F(ChromeDirectSocketsUdpIsolatedWebAppTest,
UdpServerFailsWithoutPNAPermission) {
content::RenderFrameHost* app_frame =
InstallAndOpenIsolatedWebApp(/*with_pna=*/false);
constexpr std::string_view kUdpBoundPna = R"(
(async () => {
const socket = new UDPSocket({ localAddress: "0.0.0.0" });
await socket.opened;
})();
)";
ASSERT_THAT(EvalJs(app_frame, kUdpBoundPna),
ErrorIs(PrivateNetworkAccessBlocked()));
}
IN_PROC_BROWSER_TEST_F(ChromeDirectSocketsUdpIsolatedWebAppTest,
UdpServerFailsWithoutPNAContentSetting) {
content::RenderFrameHost* app_frame =
InstallAndOpenIsolatedWebApp(/*with_pna=*/true);
HostContentSettingsMapFactory::GetForProfile(profile())
->SetDefaultContentSetting(
ContentSettingsType::DIRECT_SOCKETS_PRIVATE_NETWORK_ACCESS,
ContentSetting::CONTENT_SETTING_BLOCK);
constexpr std::string_view kUdpBoundPna = R"(
(async () => {
const socket = new UDPSocket({ localAddress: "0.0.0.0" });
await socket.opened;
})();
)";
ASSERT_THAT(EvalJs(app_frame, kUdpBoundPna),
ErrorIs(PrivateNetworkAccessBlocked()));
}
IN_PROC_BROWSER_TEST_F(ChromeDirectSocketsUdpIsolatedWebAppTest,
UdpServerPortRestrictions) {
content::RenderFrameHost* app_frame =
InstallAndOpenIsolatedWebApp(/*with_pna=*/true);
constexpr std::string_view kUdpBoundPortBelow1024 = R"(
(async () => {
const socket = new UDPSocket({ localAddress: "0.0.0.0", localPort: 455 });
await socket.opened;
})();
)";
ASSERT_THAT(EvalJs(app_frame, kUdpBoundPortBelow1024),
ErrorIs(AccessBlocked()));
constexpr std::string_view kUdpBoundPortNumberHighEnough = R"(
(async () => {
const socket = new UDPSocket({ localAddress: "0.0.0.0", localPort: 2558 });
await socket.opened;
})();
)";
ASSERT_THAT(EvalJs(app_frame, kUdpBoundPortNumberHighEnough), IsOk());
}
using ChromeDirectSocketsTcpServerIsolatedWebAppTest = IsolatedWebAppApiTest;
using ChromeDirectSocketsTcpServerIsolatedWebAppSharedWorkerTest =
IsolatedWebAppSharedWorkerApiTest;
using ChromeDirectSocketsTcpServerIsolatedWebAppServiceWorkerTest =
IsolatedWebAppServiceWorkerApiTest;
IN_PROC_BROWSER_TEST_F(ChromeDirectSocketsTcpServerIsolatedWebAppTest,
TcpServerExchangePacketWithTcpSocket) {
content::RenderFrameHost* app_frame = InstallAndOpenIsolatedWebApp();
EXPECT_THAT(EvalJs(app_frame, kTcpServerExchangePacketWithTcpScript), IsOk());
}
IN_PROC_BROWSER_TEST_F(
ChromeDirectSocketsTcpServerIsolatedWebAppSharedWorkerTest,
TcpServerExchangePacketWithTcpSocket) {
const std::string shared_worker_script = base::StringPrintf(
kSharedWorkerScriptTemplate, kTcpServerExchangePacketWithTcpScript);
content::RenderFrameHost* app_frame =
InstallAndOpenIsolatedWebAppWithSharedWorkerScript(shared_worker_script);
ASSERT_THAT(EvalJs(app_frame, kSharedWorkerConnect), IsOk());
}
IN_PROC_BROWSER_TEST_F(
ChromeDirectSocketsTcpServerIsolatedWebAppServiceWorkerTest,
TcpServerExchangePacketWithTcpSocket) {
const std::string service_worker_script = base::StringPrintf(
kServiceWorkerScriptTemplate, kTcpServerExchangePacketWithTcpScript);
content::RenderFrameHost* app_frame =
InstallAndOpenIsolatedWebAppWithServiceWorkerScript(
service_worker_script);
ASSERT_THAT(EvalJs(app_frame, kServiceWorkerConnect), IsOk());
}
IN_PROC_BROWSER_TEST_F(ChromeDirectSocketsTcpServerIsolatedWebAppTest,
TcpServerPortRestrictions) {
content::RenderFrameHost* app_frame =
InstallAndOpenIsolatedWebApp(/*with_pna=*/true);
constexpr std::string_view kTcpServerPortBelow32678 = R"(
(async () => {
const socket = new TCPServerSocket("0.0.0.0", { localPort: 7845 });
await socket.opened;
})();
)";
ASSERT_THAT(EvalJs(app_frame, kTcpServerPortBelow32678),
ErrorIs(AccessBlocked()));
constexpr std::string_view kTcpServerPortNumberHighEnough = R"(
(async () => {
const socket = new TCPServerSocket("0.0.0.0", { localPort: 35588 });
await socket.opened;
})();
)";
ASSERT_THAT(EvalJs(app_frame, kTcpServerPortNumberHighEnough), IsOk());
}
} // namespace