blob: b55e6fb89a107974eaf1e115340e9b16bc0952cf [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 <type_traits>
#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/json/json_writer.h"
#include "base/strings/stringprintf.h"
#include "build/build_config.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/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.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/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"
#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)
namespace {
constexpr char kHostname[] = "direct-sockets.com";
#if BUILDFLAG(ENABLE_EXTENSIONS)
std::string GenerateManifest(
absl::optional<base::Value::Dict> socket_permissions = {}) {
base::Value::Dict manifest;
manifest.Set(extensions::manifest_keys::kName,
"Direct Sockets in Chrome Apps");
manifest.Set(extensions::manifest_keys::kManifestVersion, 2);
manifest.Set(extensions::manifest_keys::kVersion, "1.0");
base::Value::List scripts;
scripts.Append("background.js");
manifest.SetByDottedPath(
extensions::manifest_keys::kPlatformAppBackgroundScripts,
std::move(scripts));
if (socket_permissions) {
manifest.Set(extensions::manifest_keys::kSockets,
std::move(*socket_permissions));
}
std::string out;
base::JSONWriter::Write(manifest, &out);
return out;
}
#endif
class TestServer {
public:
virtual ~TestServer() = default;
virtual void Start() = 0;
virtual void Stop() = 0;
virtual uint16_t port() const = 0;
};
class TcpHttpTestServer : public TestServer {
public:
void Start() 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() override {
DCHECK(!udp_echo_server_);
udp_echo_server_ = std::make_unique<extensions::TestUdpEchoServer>();
net::HostPortPair host_port_pair;
ASSERT_TRUE(udp_echo_server_->Start(&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_;
absl::optional<uint16_t> port_;
};
template <typename TestHarness,
typename = std::enable_if_t<
std::is_base_of_v<content::BrowserTestBase, 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();
}
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 ExtensionApiTestWithDirectSocketsEnabled
: public extensions::ExtensionApiTest {
public:
void SetUpCommandLine(base::CommandLine* command_line) override {
command_line->AppendSwitchASCII(switches::kEnableBlinkFeatures,
"DirectSockets");
}
};
using ChromeDirectSocketsTcpApiTest =
ChromeDirectSocketsTcpTest<ExtensionApiTestWithDirectSocketsEnabled>;
IN_PROC_BROWSER_TEST_F(ChromeDirectSocketsTcpApiTest, TcpReadWrite) {
extensions::TestExtensionDir dir;
base::Value::Dict socket_permissions;
socket_permissions.SetByDottedPath("tcp.connect", "*");
dir.WriteManifest(GenerateManifest(std::move(socket_permissions)));
dir.WriteFile(FILE_PATH_LITERAL("background.js"), R"(
chrome.test.sendMessage("ready", async (message) => {
try {
const [remoteAddress, remotePort] = message.split(':');
const socket = new TCPSocket(remoteAddress, remotePort);
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 () => {
reader.read().then(packet => {
const { value, done } = packet;
chrome.test.assertFalse(done,
"ReadableStream must not be exhausted at this point.");
const message = (new TextDecoder()).decode(value);
tcpResponse += message;
if (tcpResponse.length >= kTcpMinExpectedResponseLength) {
chrome.test.assertTrue(
!!tcpResponse.match(kTcpResponsePattern),
"The data returned must match the data sent."
);
chrome.test.succeed();
} else {
readUntil();
}
});
};
readUntil();
writer.write((new TextEncoder()).encode(kTcpPacket));
} catch (e) {
chrome.test.fail(e.name + ':' + e.message);
}
});
)");
extensions::ResultCatcher catcher;
ExtensionTestMessageListener listener("ready", ReplyBehavior::kWillReply);
ASSERT_TRUE(LoadExtension(dir.UnpackedPath()));
EXPECT_TRUE(listener.WaitUntilSatisfied());
listener.Reply(base::StringPrintf("%s:%d", kHostname, test_server()->port()));
EXPECT_TRUE(catcher.GetNextResult()) << catcher.message();
}
IN_PROC_BROWSER_TEST_F(ChromeDirectSocketsTcpApiTest,
TcpFailsWithoutSocketsPermission) {
extensions::TestExtensionDir dir;
dir.WriteManifest(GenerateManifest());
dir.WriteFile(FILE_PATH_LITERAL("background.js"), R"(
chrome.test.sendMessage("ready", async (message) => {
try {
const [remoteAddress, remotePort] = message.split(':');
const socket = new TCPSocket(remoteAddress, remotePort);
await chrome.test.assertPromiseRejects(
socket.opened,
"InvalidAccessError: Access to the requested host is blocked."
);
chrome.test.succeed();
} catch (e) {
chrome.test.fail(e.name + ':' + e.message);
}
});
)");
extensions::ResultCatcher catcher;
ExtensionTestMessageListener listener("ready", ReplyBehavior::kWillReply);
ASSERT_TRUE(LoadExtension(dir.UnpackedPath()));
ASSERT_TRUE(listener.WaitUntilSatisfied());
listener.Reply(base::StringPrintf("%s:%d", kHostname, 0));
EXPECT_TRUE(catcher.GetNextResult()) << catcher.message();
}
using ChromeDirectSocketsUdpApiTest =
ChromeDirectSocketsUdpTest<ExtensionApiTestWithDirectSocketsEnabled>;
IN_PROC_BROWSER_TEST_F(ChromeDirectSocketsUdpApiTest, UdpReadWrite) {
extensions::TestExtensionDir dir;
base::Value::Dict socket_permissions;
socket_permissions.SetByDottedPath("udp.send", "*");
dir.WriteManifest(GenerateManifest(std::move(socket_permissions)));
dir.WriteFile(FILE_PATH_LITERAL("background.js"), R"(
chrome.test.sendMessage("ready", async (message) => {
try {
const [remoteAddress, remotePort] = message.split(':');
const socket = new UDPSocket({ remoteAddress, remotePort });
const { readable, writable } = await socket.opened;
const reader = readable.getReader();
const writer = writable.getWriter();
const kUdpMessage = "udp_message";
reader.read().then(packet => {
const { value, done } = packet;
chrome.test.assertFalse(done,
"ReadableStream must not be exhausted at this point.");
const { data } = value;
chrome.test.assertEq((new TextDecoder()).decode(data), kUdpMessage,
"The data returned must exactly match the data sent.");
chrome.test.succeed();
});
writer.write({
data: (new TextEncoder()).encode(kUdpMessage)
});
} catch (e) {
chrome.test.fail(e.name + ':' + e.message);
}
});
)");
extensions::ResultCatcher catcher;
ExtensionTestMessageListener listener("ready", ReplyBehavior::kWillReply);
ASSERT_TRUE(LoadExtension(dir.UnpackedPath()));
EXPECT_TRUE(listener.WaitUntilSatisfied());
listener.Reply(base::StringPrintf("%s:%d", kHostname, test_server()->port()));
EXPECT_TRUE(catcher.GetNextResult()) << catcher.message();
}
IN_PROC_BROWSER_TEST_F(ChromeDirectSocketsUdpApiTest,
UdpFailsWithoutSocketsPermission) {
extensions::TestExtensionDir dir;
dir.WriteManifest(GenerateManifest());
dir.WriteFile(FILE_PATH_LITERAL("background.js"), R"(
chrome.test.sendMessage("ready", async (message) => {
try {
const [remoteAddress, remotePort] = message.split(':');
const socket = new UDPSocket({ remoteAddress, remotePort });
await chrome.test.assertPromiseRejects(
socket.opened,
"InvalidAccessError: Access to the requested host is blocked."
);
chrome.test.succeed();
} catch (e) {
chrome.test.fail(e.name + ':' + e.message);
}
});
)");
extensions::ResultCatcher catcher;
ExtensionTestMessageListener listener("ready", ReplyBehavior::kWillReply);
ASSERT_TRUE(LoadExtension(dir.UnpackedPath()));
ASSERT_TRUE(listener.WaitUntilSatisfied());
listener.Reply(base::StringPrintf("%s:%d", kHostname, 0));
EXPECT_TRUE(catcher.GetNextResult()) << catcher.message();
}
IN_PROC_BROWSER_TEST_F(ChromeDirectSocketsUdpApiTest,
UdpServerFailsWithoutSocketsSendToPermission) {
extensions::TestExtensionDir dir;
base::Value::Dict socket_permissions;
socket_permissions.SetByDottedPath("udp.bind", "*");
dir.WriteManifest(GenerateManifest(std::move(socket_permissions)));
dir.WriteFile(FILE_PATH_LITERAL("background.js"), R"(
chrome.test.sendMessage("ready", async (message) => {
try {
const socket = new UDPSocket({ localAddress : message });
await chrome.test.assertPromiseRejects(
socket.opened,
"InvalidAccessError: Access to the requested host is blocked."
);
chrome.test.succeed();
} catch (e) {
chrome.test.fail(e.name + ':' + e.message);
}
});
)");
extensions::ResultCatcher catcher;
ExtensionTestMessageListener listener("ready", ReplyBehavior::kWillReply);
ASSERT_TRUE(LoadExtension(dir.UnpackedPath()));
ASSERT_TRUE(listener.WaitUntilSatisfied());
listener.Reply("127.0.0.1");
EXPECT_TRUE(catcher.GetNextResult()) << catcher.message();
}
IN_PROC_BROWSER_TEST_F(ChromeDirectSocketsUdpApiTest, UdpServerReadWrite) {
extensions::TestExtensionDir dir;
base::Value::Dict socket_permissions;
socket_permissions.SetByDottedPath("udp.bind", "*");
socket_permissions.SetByDottedPath("udp.send", "*");
dir.WriteManifest(GenerateManifest(std::move(socket_permissions)));
dir.WriteFile(FILE_PATH_LITERAL("background.js"), R"(
chrome.test.sendMessage("ready", async (message) => {
try {
const [clientAddress, clientPort] = message.split(':');
const socket = new UDPSocket({ localAddress: "127.0.0.1" });
const { readable, writable } = await socket.opened;
const reader = readable.getReader();
const writer = writable.getWriter();
const kUdpMessage = "udp_message";
reader.read().then(packet => {
const { value, done } = packet;
chrome.test.assertFalse(done,
"ReadableStream must not be exhausted at this point.");
const { data, remoteAddress, remotePort } = value;
chrome.test.assertEq((new TextDecoder()).decode(data), kUdpMessage,
"The data returned must exactly match the data sent.");
chrome.test.assertEq(remoteAddress, "127.0.0.1");
chrome.test.assertEq(remotePort, parseInt(clientPort));
chrome.test.succeed();
});
writer.write({
data: (new TextEncoder()).encode(kUdpMessage),
remoteAddress: clientAddress,
remotePort: clientPort,
});
} catch (e) {
chrome.test.fail(e.name + ':' + e.message);
}
});
)");
extensions::ResultCatcher catcher;
ExtensionTestMessageListener listener("ready", ReplyBehavior::kWillReply);
ASSERT_TRUE(LoadExtension(dir.UnpackedPath()));
ASSERT_TRUE(listener.WaitUntilSatisfied());
listener.Reply(base::StringPrintf("%s:%d", kHostname, test_server()->port()));
EXPECT_TRUE(catcher.GetNextResult()) << catcher.message();
}
#endif
using IsolatedWebAppTestHarnessWithDirectSocketsEnabled =
web_app::IsolatedWebAppBrowserTestHarness;
using ChromeDirectSocketsTcpIsolatedWebAppTest = ChromeDirectSocketsTcpTest<
IsolatedWebAppTestHarnessWithDirectSocketsEnabled>;
IN_PROC_BROWSER_TEST_F(ChromeDirectSocketsTcpIsolatedWebAppTest, TcpReadWrite) {
// Install & open the IWA.
std::unique_ptr<net::EmbeddedTestServer> isolated_web_app_dev_server =
CreateAndStartServer(FILE_PATH_LITERAL("web_apps/simple_isolated_app"));
web_app::IsolatedWebAppUrlInfo url_info = InstallDevModeProxyIsolatedWebApp(
isolated_web_app_dev_server->GetOrigin());
content::RenderFrameHost* app_frame = OpenApp(url_info.app_id());
// Run the echo script.
constexpr base::StringPiece kTcpSendReceiveHttpScript = R"(
(async () => {
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) {
console.log("ReadableStream must not be exhausted at this point.");
return false;
}
const message = (new TextDecoder()).decode(value);
tcpResponse += message;
if (tcpResponse.length >= kTcpMinExpectedResponseLength) {
if (!tcpResponse.match(kTcpResponsePattern)) {
console.log("The data returned must match the data sent.");
return false;
}
return true;
} else {
return await readUntil();
}
};
writer.write((new TextEncoder()).encode(kTcpPacket));
return await readUntil();
} catch (err) {
console.log(err);
return false;
}
})();
)";
ASSERT_TRUE(
EvalJs(app_frame, content::JsReplace(kTcpSendReceiveHttpScript, kHostname,
test_server()->port()))
.ExtractBool());
}
using ChromeDirectSocketsUdpIsolatedWebAppTest = ChromeDirectSocketsUdpTest<
IsolatedWebAppTestHarnessWithDirectSocketsEnabled>;
IN_PROC_BROWSER_TEST_F(ChromeDirectSocketsUdpIsolatedWebAppTest, UdpReadWrite) {
// Install & open the IWA.
std::unique_ptr<net::EmbeddedTestServer> isolated_web_app_dev_server =
CreateAndStartServer(FILE_PATH_LITERAL("web_apps/simple_isolated_app"));
web_app::IsolatedWebAppUrlInfo url_info = InstallDevModeProxyIsolatedWebApp(
isolated_web_app_dev_server->GetOrigin());
content::RenderFrameHost* app_frame = OpenApp(url_info.app_id());
// Run the echo script.
constexpr base::StringPiece kUdpSendReceiveEchoScript = R"(
(async () => {
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) {
return false;
}
const { data } = value;
if ((new TextDecoder()).decode(data) !== kUdpMessage) {
return false;
}
return true;
});
} catch (err) {
console.log(err);
return false;
}
})();
)";
ASSERT_TRUE(
EvalJs(app_frame, content::JsReplace(kUdpSendReceiveEchoScript, kHostname,
test_server()->port()))
.ExtractBool());
}
IN_PROC_BROWSER_TEST_F(ChromeDirectSocketsUdpIsolatedWebAppTest,
UdpServerReadWrite) {
// Install & open the IWA.
std::unique_ptr<net::EmbeddedTestServer> isolated_web_app_dev_server =
CreateAndStartServer(FILE_PATH_LITERAL("web_apps/simple_isolated_app"));
web_app::IsolatedWebAppUrlInfo url_info = InstallDevModeProxyIsolatedWebApp(
isolated_web_app_dev_server->GetOrigin());
content::RenderFrameHost* app_frame = OpenApp(url_info.app_id());
// Run the echo script.
constexpr base::StringPiece kUdpServerSendReceiveEchoScript = R"(
(async () => {
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) {
return false;
}
const { data, remoteAddress, remotePort } = value;
if ((new TextDecoder()).decode(data) !== kUdpMessage) {
return false;
}
if (remoteAddress !== "127.0.0.1") {
return false;
}
if (remotePort !== $2) {
return false;
}
return true;
});
} catch (err) {
console.log(err);
return false;
}
})();
)";
ASSERT_TRUE(
EvalJs(app_frame, content::JsReplace(kUdpServerSendReceiveEchoScript,
kHostname, test_server()->port()))
.ExtractBool());
}
} // namespace