diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /toolkit/components/remote | |
parent | Initial commit. (diff) | |
download | thunderbird-upstream.tar.xz thunderbird-upstream.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
44 files changed, 4435 insertions, 0 deletions
diff --git a/toolkit/components/remote/RemoteUtils.cpp b/toolkit/components/remote/RemoteUtils.cpp new file mode 100644 index 0000000000..f7677f92b9 --- /dev/null +++ b/toolkit/components/remote/RemoteUtils.cpp @@ -0,0 +1,102 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:expandtab:shiftwidth=2:tabstop=8: + */ +/* vim:set ts=8 sw=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <limits.h> + +#include "RemoteUtils.h" + +#ifdef IS_BIG_ENDIAN +# define TO_LITTLE_ENDIAN32(x) \ + ((((x)&0xff000000) >> 24) | (((x)&0x00ff0000) >> 8) | \ + (((x)&0x0000ff00) << 8) | (((x)&0x000000ff) << 24)) +#else +# define TO_LITTLE_ENDIAN32(x) (x) +#endif + +#ifndef MAX_PATH +# ifdef PATH_MAX +# define MAX_PATH PATH_MAX +# else +# define MAX_PATH 1024 +# endif +#endif + +/* like strcpy, but return the char after the final null */ +static char* estrcpy(const char* s, char* d) { + while (*s) *d++ = *s++; + + *d++ = '\0'; + return d; +} + +/* Construct a command line from given args and desktop startup ID. + * Returned buffer must be released by free(). + */ +char* ConstructCommandLine(int32_t argc, char** argv, const char* aStartupToken, + int* aCommandLineLength) { + char cwdbuf[MAX_PATH]; + if (!getcwd(cwdbuf, MAX_PATH)) return nullptr; + + // the commandline property is constructed as an array of int32_t + // followed by a series of null-terminated strings: + // + // [argc][offsetargv0][offsetargv1...]<workingdir>\0<argv[0]>\0argv[1]...\0 + // (offset is from the beginning of the buffer) + + static char startupTokenPrefix[] = " STARTUP_TOKEN="; + + int32_t argvlen = strlen(cwdbuf); + for (int i = 0; i < argc; ++i) { + int32_t len = strlen(argv[i]); + if (i == 0 && aStartupToken) { + len += sizeof(startupTokenPrefix) - 1 + strlen(aStartupToken); + } + argvlen += len; + } + + auto* buffer = + (int32_t*)malloc(argvlen + argc + 1 + sizeof(int32_t) * (argc + 1)); + if (!buffer) return nullptr; + + buffer[0] = TO_LITTLE_ENDIAN32(argc); + + auto* bufend = (char*)(buffer + argc + 1); + + bufend = estrcpy(cwdbuf, bufend); + + for (int i = 0; i < argc; ++i) { + buffer[i + 1] = TO_LITTLE_ENDIAN32(bufend - ((char*)buffer)); + bufend = estrcpy(argv[i], bufend); + if (i == 0 && aStartupToken) { + bufend = estrcpy(startupTokenPrefix, bufend - 1); + bufend = estrcpy(aStartupToken, bufend - 1); + } + } + +#ifdef DEBUG_command_line + int32_t debug_argc = TO_LITTLE_ENDIAN32(*buffer); + char* debug_workingdir = (char*)(buffer + argc + 1); + + printf( + "Sending command line:\n" + " working dir: %s\n" + " argc:\t%i", + debug_workingdir, debug_argc); + + int32_t* debug_offset = buffer + 1; + for (int debug_i = 0; debug_i < debug_argc; ++debug_i) + printf(" argv[%i]:\t%s\n", debug_i, + ((char*)buffer) + TO_LITTLE_ENDIAN32(debug_offset[debug_i])); +#endif + + *aCommandLineLength = bufend - reinterpret_cast<char*>(buffer); + return reinterpret_cast<char*>(buffer); +} diff --git a/toolkit/components/remote/RemoteUtils.h b/toolkit/components/remote/RemoteUtils.h new file mode 100644 index 0000000000..2d21293eb5 --- /dev/null +++ b/toolkit/components/remote/RemoteUtils.h @@ -0,0 +1,31 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef TOOLKIT_COMPONENTS_REMOTE_REMOTEUTILS_H_ +#define TOOLKIT_COMPONENTS_REMOTE_REMOTEUTILS_H_ + +#include "nsString.h" +#if defined XP_WIN +# include "WinUtils.h" +#endif + +#if defined XP_WIN || defined XP_MACOSX +static void BuildClassName(const char* aProgram, const char* aProfile, + nsString& aClassName) { + aClassName.AppendPrintf("Mozilla_%s", aProgram); +# if defined XP_WIN + nsString pfn = mozilla::widget::WinUtils::GetPackageFamilyName(); + if (!pfn.IsEmpty()) { + aClassName.AppendPrintf("_%S", static_cast<const wchar_t*>(pfn.get())); + } +# endif + aClassName.AppendPrintf("_%s_RemoteWindow", aProfile); +} +#endif + +char* ConstructCommandLine(int32_t argc, char** argv, const char* aStartupToken, + int* aCommandLineLength); + +#endif // TOOLKIT_COMPONENTS_REMOTE_REMOTEUTILS_H_ diff --git a/toolkit/components/remote/WinRemoteMessage.cpp b/toolkit/components/remote/WinRemoteMessage.cpp new file mode 100644 index 0000000000..98f425ba1a --- /dev/null +++ b/toolkit/components/remote/WinRemoteMessage.cpp @@ -0,0 +1,124 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsCommandLine.h" +#include "mozilla/CmdLineAndEnvUtils.h" +#include "WinRemoteMessage.h" + +using namespace mozilla; + +WinRemoteMessageSender::WinRemoteMessageSender(const char* aCommandLine) + : mData({static_cast<DWORD>(WinRemoteMessageVersion::CommandLineOnly)}), + mUtf8Buffer(aCommandLine) { + mUtf8Buffer.Append('\0'); + + char* mutableBuffer; + mData.cbData = mUtf8Buffer.GetMutableData(&mutableBuffer); + mData.lpData = mutableBuffer; +} + +WinRemoteMessageSender::WinRemoteMessageSender(const char* aCommandLine, + const char* aWorkingDir) + : mData({static_cast<DWORD>( + WinRemoteMessageVersion::CommandLineAndWorkingDir)}), + mUtf8Buffer(aCommandLine) { + mUtf8Buffer.Append('\0'); + mUtf8Buffer.Append(aWorkingDir); + mUtf8Buffer.Append('\0'); + + char* mutableBuffer; + mData.cbData = mUtf8Buffer.GetMutableData(&mutableBuffer); + mData.lpData = mutableBuffer; +} + +WinRemoteMessageSender::WinRemoteMessageSender(const wchar_t* aCommandLine, + const wchar_t* aWorkingDir) + : mData({static_cast<DWORD>( + WinRemoteMessageVersion::CommandLineAndWorkingDirInUtf16)}), + mUtf16Buffer(aCommandLine) { + mUtf16Buffer.Append(u'\0'); + mUtf16Buffer.Append(aWorkingDir); + mUtf16Buffer.Append(u'\0'); + + char16_t* mutableBuffer; + mData.cbData = mUtf16Buffer.GetMutableData(&mutableBuffer) * sizeof(char16_t); + mData.lpData = mutableBuffer; +} + +COPYDATASTRUCT* WinRemoteMessageSender::CopyData() { return &mData; } + +nsresult WinRemoteMessageReceiver::ParseV0(const nsACString& aBuffer) { + CommandLineParserWin<char> parser; + parser.HandleCommandLine(aBuffer); + + mCommandLine = new nsCommandLine(); + return mCommandLine->Init(parser.Argc(), parser.Argv(), nullptr, + nsICommandLine::STATE_REMOTE_AUTO); +} + +nsresult WinRemoteMessageReceiver::ParseV1(const nsACString& aBuffer) { + CommandLineParserWin<char> parser; + size_t cch = parser.HandleCommandLine(aBuffer); + ++cch; // skip a null char + + nsCOMPtr<nsIFile> workingDir; + if (cch < aBuffer.Length()) { + NS_NewLocalFile(NS_ConvertUTF8toUTF16(Substring(aBuffer, cch)), false, + getter_AddRefs(workingDir)); + } + + mCommandLine = new nsCommandLine(); + return mCommandLine->Init(parser.Argc(), parser.Argv(), workingDir, + nsICommandLine::STATE_REMOTE_AUTO); +} + +nsresult WinRemoteMessageReceiver::ParseV2(const nsAString& aBuffer) { + CommandLineParserWin<char16_t> parser; + size_t cch = parser.HandleCommandLine(aBuffer); + ++cch; // skip a null char + + nsCOMPtr<nsIFile> workingDir; + if (cch < aBuffer.Length()) { + NS_NewLocalFile(Substring(aBuffer, cch), false, getter_AddRefs(workingDir)); + } + + int argc = parser.Argc(); + Vector<nsAutoCString> utf8args; + if (!utf8args.reserve(argc)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + UniquePtr<const char*[]> argv(new const char*[argc]); + for (int i = 0; i < argc; ++i) { + utf8args.infallibleAppend(NS_ConvertUTF16toUTF8(parser.Argv()[i])); + argv[i] = utf8args[i].get(); + } + + mCommandLine = new nsCommandLine(); + return mCommandLine->Init(argc, argv.get(), workingDir, + nsICommandLine::STATE_REMOTE_AUTO); +} + +nsresult WinRemoteMessageReceiver::Parse(const COPYDATASTRUCT* aMessageData) { + switch (static_cast<WinRemoteMessageVersion>(aMessageData->dwData)) { + case WinRemoteMessageVersion::CommandLineOnly: + return ParseV0(nsDependentCSubstring( + reinterpret_cast<char*>(aMessageData->lpData), aMessageData->cbData)); + case WinRemoteMessageVersion::CommandLineAndWorkingDir: + return ParseV1(nsDependentCSubstring( + reinterpret_cast<char*>(aMessageData->lpData), aMessageData->cbData)); + case WinRemoteMessageVersion::CommandLineAndWorkingDirInUtf16: + return ParseV2(nsDependentSubstring( + reinterpret_cast<char16_t*>(aMessageData->lpData), + aMessageData->cbData / sizeof(char16_t))); + default: + MOZ_ASSERT_UNREACHABLE("Unsupported message version"); + return NS_ERROR_FAILURE; + } +} + +nsICommandLineRunner* WinRemoteMessageReceiver::CommandLineRunner() { + return mCommandLine; +} diff --git a/toolkit/components/remote/WinRemoteMessage.h b/toolkit/components/remote/WinRemoteMessage.h new file mode 100644 index 0000000000..948c6db310 --- /dev/null +++ b/toolkit/components/remote/WinRemoteMessage.h @@ -0,0 +1,69 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef __WinRemoteMessage_h__ +#define __WinRemoteMessage_h__ + +#include <windows.h> + +#include "nsICommandLineRunner.h" +#include "nsCOMPtr.h" +#include "nsString.h" + +// This version defines the format of COPYDATASTRUCT::lpData in a message of +// WM_COPYDATA. +// Always use the latest version for production use because the older versions +// have a bug that a non-ascii character in a utf-8 message cannot be parsed +// correctly (bug 1650637). We keep the older versions for backward +// compatibility and the testing purpose only. +enum class WinRemoteMessageVersion : uint32_t { + // "<CommandLine>\0" in utf8 + CommandLineOnly = 0, + // "<CommandLine>\0<WorkingDir>\0" in utf8 + CommandLineAndWorkingDir, + // L"<CommandLine>\0<WorkingDir>\0" in utf16 + CommandLineAndWorkingDirInUtf16, +}; + +class WinRemoteMessageSender final { + COPYDATASTRUCT mData; + nsAutoString mUtf16Buffer; + nsAutoCString mUtf8Buffer; + + public: + WinRemoteMessageSender(const wchar_t* aCommandLine, + const wchar_t* aWorkingDir); + + WinRemoteMessageSender(const WinRemoteMessageSender&) = delete; + WinRemoteMessageSender(WinRemoteMessageSender&&) = delete; + WinRemoteMessageSender& operator=(const WinRemoteMessageSender&) = delete; + WinRemoteMessageSender& operator=(WinRemoteMessageSender&&) = delete; + + COPYDATASTRUCT* CopyData(); + + // Constructors for the old formats. Testing purpose only. + explicit WinRemoteMessageSender(const char* aCommandLine); + WinRemoteMessageSender(const char* aCommandLine, const char* aWorkingDir); +}; + +class WinRemoteMessageReceiver final { + nsCOMPtr<nsICommandLineRunner> mCommandLine; + + nsresult ParseV0(const nsACString& aBuffer); + nsresult ParseV1(const nsACString& aBuffer); + nsresult ParseV2(const nsAString& aBuffer); + + public: + WinRemoteMessageReceiver() = default; + WinRemoteMessageReceiver(const WinRemoteMessageReceiver&) = delete; + WinRemoteMessageReceiver(WinRemoteMessageReceiver&&) = delete; + WinRemoteMessageReceiver& operator=(const WinRemoteMessageReceiver&) = delete; + WinRemoteMessageReceiver& operator=(WinRemoteMessageReceiver&&) = delete; + + nsresult Parse(const COPYDATASTRUCT* aMessageData); + nsICommandLineRunner* CommandLineRunner(); +}; + +#endif // __WinRemoteMessage_h__ diff --git a/toolkit/components/remote/moz.build b/toolkit/components/remote/moz.build new file mode 100644 index 0000000000..d3bab6cf9a --- /dev/null +++ b/toolkit/components/remote/moz.build @@ -0,0 +1,53 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +with Files("**"): + BUG_COMPONENT = ("Toolkit", "Startup and Profile System") + +SOURCES += [ + "nsRemoteService.cpp", +] + +if CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk": + SOURCES += [ + "nsUnixRemoteServer.cpp", + "RemoteUtils.cpp", + ] + if CONFIG["MOZ_ENABLE_DBUS"]: + SOURCES += [ + "nsDBusRemoteClient.cpp", + "nsDBusRemoteServer.cpp", + ] + CXXFLAGS += CONFIG["MOZ_DBUS_GLIB_CFLAGS"] + EXPORTS += [ + "nsUnixRemoteServer.h", + "RemoteUtils.h", + ] + else: + SOURCES += [ + "nsGTKRemoteServer.cpp", + "nsXRemoteClient.cpp", + "nsXRemoteServer.cpp", + ] + CXXFLAGS += CONFIG["MOZ_GTK3_CFLAGS"] + +if CONFIG["MOZ_WIDGET_TOOLKIT"] == "windows": + SOURCES += [ + "nsWinRemoteClient.cpp", + "nsWinRemoteServer.cpp", + "WinRemoteMessage.cpp", + ] +if CONFIG["MOZ_WIDGET_TOOLKIT"] == "cocoa": + SOURCES += [ + "nsMacRemoteClient.mm", + "nsMacRemoteServer.mm", + ] + +LOCAL_INCLUDES += [ + "../../profile", + "../../xre", +] +FINAL_LIBRARY = "xul" diff --git a/toolkit/components/remote/nsDBusRemoteClient.cpp b/toolkit/components/remote/nsDBusRemoteClient.cpp new file mode 100644 index 0000000000..69e8d07c72 --- /dev/null +++ b/toolkit/components/remote/nsDBusRemoteClient.cpp @@ -0,0 +1,188 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:expandtab:shiftwidth=2:tabstop=8: + */ +/* vim:set ts=8 sw=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsDBusRemoteClient.h" +#include "RemoteUtils.h" +#include "mozilla/XREAppData.h" +#include "mozilla/Logging.h" +#include "mozilla/Base64.h" +#include "nsPrintfCString.h" + +#include <dlfcn.h> +#include <dbus/dbus-glib-lowlevel.h> + +#undef LOG +#ifdef MOZ_LOGGING +static mozilla::LazyLogModule sRemoteLm("nsDBusRemoteClient"); +# define LOG(str, ...) \ + MOZ_LOG(sRemoteLm, mozilla::LogLevel::Debug, (str, ##__VA_ARGS__)) +#else +# define LOG(...) +#endif + +nsDBusRemoteClient::nsDBusRemoteClient() { + mConnection = nullptr; + LOG("nsDBusRemoteClient::nsDBusRemoteClient"); +} + +nsDBusRemoteClient::~nsDBusRemoteClient() { + LOG("nsDBusRemoteClient::~nsDBusRemoteClient"); + Shutdown(); +} + +nsresult nsDBusRemoteClient::Init() { + LOG("nsDBusRemoteClient::Init"); + + if (mConnection) return NS_OK; + + mConnection = + already_AddRefed<DBusConnection>(dbus_bus_get(DBUS_BUS_SESSION, nullptr)); + if (!mConnection) { + LOG(" failed to get DBus session"); + return NS_ERROR_FAILURE; + } + + dbus_connection_set_exit_on_disconnect(mConnection, false); + dbus_connection_setup_with_g_main(mConnection, nullptr); + + return NS_OK; +} + +void nsDBusRemoteClient::Shutdown(void) { + LOG("nsDBusRemoteClient::Shutdown"); + // This connection is owned by libdbus and we don't need to close it + mConnection = nullptr; +} + +nsresult nsDBusRemoteClient::SendCommandLine( + const char* aProgram, const char* aProfile, int32_t argc, char** argv, + const char* aStartupToken, char** aResponse, bool* aWindowFound) { + NS_ENSURE_TRUE(aProgram, NS_ERROR_INVALID_ARG); + + LOG("nsDBusRemoteClient::SendCommandLine"); + + int commandLineLength; + char* commandLine = + ConstructCommandLine(argc, argv, aStartupToken, &commandLineLength); + if (!commandLine) { + LOG(" failed to create command line"); + return NS_ERROR_FAILURE; + } + + nsresult rv = + DoSendDBusCommandLine(aProgram, aProfile, commandLine, commandLineLength); + free(commandLine); + + *aWindowFound = NS_SUCCEEDED(rv); + + LOG("DoSendDBusCommandLine %s", NS_SUCCEEDED(rv) ? "OK" : "FAILED"); + return rv; +} + +bool nsDBusRemoteClient::GetRemoteDestinationName(const char* aProgram, + const char* aProfile, + nsCString& aDestinationName) { + // We have a profile name - just create the destination. + // D-Bus names can contain only [a-z][A-Z][0-9]_ + // characters so adjust the profile string properly. + nsAutoCString profileName; + nsresult rv = mozilla::Base64Encode(nsAutoCString(aProfile), profileName); + NS_ENSURE_SUCCESS(rv, false); + + mozilla::XREAppData::SanitizeNameForDBus(profileName); + + aDestinationName = + nsPrintfCString("org.mozilla.%s.%s", aProgram, profileName.get()); + if (aDestinationName.Length() > DBUS_MAXIMUM_NAME_LENGTH) + aDestinationName.Truncate(DBUS_MAXIMUM_NAME_LENGTH); + + static auto sDBusValidateBusName = (bool (*)(const char*, DBusError*))dlsym( + RTLD_DEFAULT, "dbus_validate_bus_name"); + if (!sDBusValidateBusName) { + LOG(" failed to get dbus_validate_bus_name()"); + return false; + } + + if (!sDBusValidateBusName(aDestinationName.get(), nullptr)) { + // We don't have a valid busName yet - try to create a default one. + aDestinationName = + nsPrintfCString("org.mozilla.%s.%s", aProgram, "default"); + if (!sDBusValidateBusName(aDestinationName.get(), nullptr)) { + // We failed completelly to get a valid bus name - just quit + // to prevent crash at dbus_bus_request_name(). + LOG(" failed to validate profile DBus name"); + return false; + } + } + + return true; +} + +nsresult nsDBusRemoteClient::DoSendDBusCommandLine(const char* aProgram, + const char* aProfile, + const char* aBuffer, + int aLength) { + LOG("nsDBusRemoteClient::DoSendDBusCommandLine()"); + + nsAutoCString appName(aProgram); + mozilla::XREAppData::SanitizeNameForDBus(appName); + + nsAutoCString destinationName; + if (!GetRemoteDestinationName(appName.get(), aProfile, destinationName)) { + LOG(" failed to get remote destination name"); + return NS_ERROR_FAILURE; + } + + nsAutoCString pathName; + pathName = nsPrintfCString("/org/mozilla/%s/Remote", appName.get()); + + static auto sDBusValidatePathName = (bool (*)(const char*, DBusError*))dlsym( + RTLD_DEFAULT, "dbus_validate_path"); + if (!sDBusValidatePathName || + !sDBusValidatePathName(pathName.get(), nullptr)) { + LOG(" failed to validate path name"); + return NS_ERROR_FAILURE; + } + + nsAutoCString remoteInterfaceName; + remoteInterfaceName = nsPrintfCString("org.mozilla.%s", appName.get()); + + LOG(" DBus destination: %s\n", destinationName.get()); + LOG(" DBus path: %s\n", pathName.get()); + LOG(" DBus interface: %s\n", remoteInterfaceName.get()); + + RefPtr<DBusMessage> msg = + already_AddRefed<DBusMessage>(dbus_message_new_method_call( + destinationName.get(), + pathName.get(), // object to call on + remoteInterfaceName.get(), // interface to call on + "OpenURL")); // method name + if (!msg) { + LOG(" failed to create DBus message"); + return NS_ERROR_FAILURE; + } + + // append arguments + if (!dbus_message_append_args(msg, DBUS_TYPE_ARRAY, DBUS_TYPE_BYTE, &aBuffer, + aLength, DBUS_TYPE_INVALID)) { + LOG(" failed to create DBus message"); + return NS_ERROR_FAILURE; + } + + // send message and get a handle for a reply + RefPtr<DBusMessage> reply = already_AddRefed<DBusMessage>( + dbus_connection_send_with_reply_and_block(mConnection, msg, -1, nullptr)); + +#ifdef MOZ_LOGGING + if (!reply) { + LOG(" failed to get DBus reply"); + } +#endif + + return reply ? NS_OK : NS_ERROR_FAILURE; +} diff --git a/toolkit/components/remote/nsDBusRemoteClient.h b/toolkit/components/remote/nsDBusRemoteClient.h new file mode 100644 index 0000000000..fdb4dbc665 --- /dev/null +++ b/toolkit/components/remote/nsDBusRemoteClient.h @@ -0,0 +1,34 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DBusRemoteClient_h__ +#define DBusRemoteClient_h__ + +#include "nsRemoteClient.h" +#include "mozilla/DBusHelpers.h" +#include "mozilla/RefPtr.h" +#include "nsStringFwd.h" +#include "nscore.h" + +class nsDBusRemoteClient : public nsRemoteClient { + public: + nsDBusRemoteClient(); + ~nsDBusRemoteClient(); + + nsresult Init() override; + nsresult SendCommandLine(const char* aProgram, const char* aProfile, + int32_t argc, char** argv, const char* aStartupToken, + char** aResponse, bool* aSucceeded) override; + void Shutdown(); + + private: + bool GetRemoteDestinationName(const char* aProgram, const char* aProfile, + nsCString& aDestinationName); + nsresult DoSendDBusCommandLine(const char* aProgram, const char* aProfile, + const char* aBuffer, int aLength); + RefPtr<DBusConnection> mConnection; +}; + +#endif // DBusRemoteClient_h__ diff --git a/toolkit/components/remote/nsDBusRemoteServer.cpp b/toolkit/components/remote/nsDBusRemoteServer.cpp new file mode 100644 index 0000000000..34167177d9 --- /dev/null +++ b/toolkit/components/remote/nsDBusRemoteServer.cpp @@ -0,0 +1,214 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:expandtab:shiftwidth=2:tabstop=2: + */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsDBusRemoteServer.h" + +#include "nsCOMPtr.h" +#include "mozilla/XREAppData.h" +#include "mozilla/Base64.h" +#include "mozilla/ScopeExit.h" +#include "nsPrintfCString.h" + +#include "nsGTKToolkit.h" + +#include <dbus/dbus.h> +#include <dbus/dbus-glib-lowlevel.h> + +#include <dlfcn.h> + +static const char* introspect_template = + "<!DOCTYPE node PUBLIC \"-//freedesktop//DTD D-BUS Object Introspection " + "1.0//EN\"\n" + "\"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd\">\n" + "<node>\n" + " <interface name=\"org.freedesktop.DBus.Introspectable\">\n" + " <method name=\"Introspect\">\n" + " <arg name=\"data\" direction=\"out\" type=\"s\"/>\n" + " </method>\n" + " </interface>\n" + " <interface name=\"org.mozilla.%s\">\n" + " <method name=\"OpenURL\">\n" + " <arg name=\"url\" direction=\"in\" type=\"ay\"/>\n" + " </method>\n" + " </interface>\n" + "</node>\n"; + +DBusHandlerResult nsDBusRemoteServer::Introspect(DBusMessage* msg) { + DBusMessage* reply = dbus_message_new_method_return(msg); + if (!reply) return DBUS_HANDLER_RESULT_NEED_MEMORY; + + nsAutoCString introspect_xml; + introspect_xml = nsPrintfCString(introspect_template, mAppName.get()); + + const char* message = introspect_xml.get(); + dbus_message_append_args(reply, DBUS_TYPE_STRING, &message, + DBUS_TYPE_INVALID); + + dbus_connection_send(mConnection, reply, nullptr); + dbus_message_unref(reply); + + return DBUS_HANDLER_RESULT_HANDLED; +} + +DBusHandlerResult nsDBusRemoteServer::OpenURL(DBusMessage* msg) { + DBusMessage* reply = nullptr; + const char* commandLine; + int length; + + if (!dbus_message_get_args(msg, nullptr, DBUS_TYPE_ARRAY, DBUS_TYPE_BYTE, + &commandLine, &length, DBUS_TYPE_INVALID) || + length == 0) { + nsAutoCString errorMsg; + errorMsg = nsPrintfCString("org.mozilla.%s.Error", mAppName.get()); + reply = dbus_message_new_error(msg, errorMsg.get(), "Wrong argument"); + } else { + guint32 timestamp = gtk_get_current_event_time(); + if (timestamp == GDK_CURRENT_TIME) { + timestamp = guint32(g_get_monotonic_time() / 1000); + } + HandleCommandLine(commandLine, timestamp); + reply = dbus_message_new_method_return(msg); + } + + dbus_connection_send(mConnection, reply, nullptr); + dbus_message_unref(reply); + + return DBUS_HANDLER_RESULT_HANDLED; +} + +DBusHandlerResult nsDBusRemoteServer::HandleDBusMessage( + DBusConnection* aConnection, DBusMessage* msg) { + NS_ASSERTION(mConnection == aConnection, "Wrong D-Bus connection."); + + const char* method = dbus_message_get_member(msg); + const char* iface = dbus_message_get_interface(msg); + + if ((strcmp("Introspect", method) == 0) && + (strcmp("org.freedesktop.DBus.Introspectable", iface) == 0)) { + return Introspect(msg); + } + + nsAutoCString ourInterfaceName; + ourInterfaceName = nsPrintfCString("org.mozilla.%s", mAppName.get()); + + if ((strcmp("OpenURL", method) == 0) && + (strcmp(ourInterfaceName.get(), iface) == 0)) { + return OpenURL(msg); + } + + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; +} + +void nsDBusRemoteServer::UnregisterDBusInterface(DBusConnection* aConnection) { + NS_ASSERTION(mConnection == aConnection, "Wrong D-Bus connection."); + // Not implemented +} + +static DBusHandlerResult message_handler(DBusConnection* conn, DBusMessage* msg, + void* user_data) { + auto interface = static_cast<nsDBusRemoteServer*>(user_data); + return interface->HandleDBusMessage(conn, msg); +} + +static void unregister(DBusConnection* conn, void* user_data) { + auto interface = static_cast<nsDBusRemoteServer*>(user_data); + interface->UnregisterDBusInterface(conn); +} + +static DBusObjectPathVTable remoteHandlersTable = { + .unregister_function = unregister, + .message_function = message_handler, +}; + +nsresult nsDBusRemoteServer::Startup(const char* aAppName, + const char* aProfileName) { + if (mConnection && dbus_connection_get_is_connected(mConnection)) { + // We're already connected so we don't need to reconnect + return NS_ERROR_ALREADY_INITIALIZED; + } + + // Don't even try to start without any application/profile name + if (!aAppName || aAppName[0] == '\0' || !aProfileName || + aProfileName[0] == '\0') + return NS_ERROR_INVALID_ARG; + + mConnection = dont_AddRef(dbus_bus_get(DBUS_BUS_SESSION, nullptr)); + if (!mConnection) { + return NS_ERROR_FAILURE; + } + auto releaseDBusConnection = + mozilla::MakeScopeExit([&] { mConnection = nullptr; }); + dbus_connection_set_exit_on_disconnect(mConnection, false); + dbus_connection_setup_with_g_main(mConnection, nullptr); + + mAppName = aAppName; + mozilla::XREAppData::SanitizeNameForDBus(mAppName); + + nsAutoCString profileName; + MOZ_TRY( + mozilla::Base64Encode(aProfileName, strlen(aProfileName), profileName)); + + mozilla::XREAppData::SanitizeNameForDBus(profileName); + + nsPrintfCString busName("org.mozilla.%s.%s", mAppName.get(), + profileName.get()); + if (busName.Length() > DBUS_MAXIMUM_NAME_LENGTH) + busName.Truncate(DBUS_MAXIMUM_NAME_LENGTH); + + static auto sDBusValidateBusName = (bool (*)(const char*, DBusError*))dlsym( + RTLD_DEFAULT, "dbus_validate_bus_name"); + if (!sDBusValidateBusName) { + return NS_ERROR_FAILURE; + } + + // We don't have a valid busName yet - try to create a default one. + if (!sDBusValidateBusName(busName.get(), nullptr)) { + busName = nsPrintfCString("org.mozilla.%s.%s", mAppName.get(), "default"); + if (!sDBusValidateBusName(busName.get(), nullptr)) { + // We failed completelly to get a valid bus name - just quit + // to prevent crash at dbus_bus_request_name(). + return NS_ERROR_FAILURE; + } + } + + DBusError err; + dbus_error_init(&err); + dbus_bus_request_name(mConnection, busName.get(), DBUS_NAME_FLAG_DO_NOT_QUEUE, + &err); + // The interface is already owned - there is another application/profile + // instance already running. + if (dbus_error_is_set(&err)) { + dbus_error_free(&err); + return NS_ERROR_FAILURE; + } + + mPathName = nsPrintfCString("/org/mozilla/%s/Remote", mAppName.get()); + static auto sDBusValidatePathName = (bool (*)(const char*, DBusError*))dlsym( + RTLD_DEFAULT, "dbus_validate_path"); + if (!sDBusValidatePathName || + !sDBusValidatePathName(mPathName.get(), nullptr)) { + return NS_ERROR_FAILURE; + } + if (!dbus_connection_register_object_path(mConnection, mPathName.get(), + &remoteHandlersTable, this)) { + return NS_ERROR_FAILURE; + } + + releaseDBusConnection.release(); + return NS_OK; +} + +void nsDBusRemoteServer::Shutdown() { + if (!mConnection) { + return; + } + + dbus_connection_unregister_object_path(mConnection, mPathName.get()); + + // dbus_connection_unref() will be called by RefPtr here. + mConnection = nullptr; +} diff --git a/toolkit/components/remote/nsDBusRemoteServer.h b/toolkit/components/remote/nsDBusRemoteServer.h new file mode 100644 index 0000000000..ce23a28c36 --- /dev/null +++ b/toolkit/components/remote/nsDBusRemoteServer.h @@ -0,0 +1,38 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:expandtab:shiftwidth=2:tabstop=2: + */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef __nsDBusRemoteServer_h__ +#define __nsDBusRemoteServer_h__ + +#include "nsRemoteServer.h" +#include "nsUnixRemoteServer.h" +#include "mozilla/DBusHelpers.h" + +class nsDBusRemoteServer final : public nsRemoteServer, + public nsUnixRemoteServer { + public: + nsDBusRemoteServer() : mConnection(nullptr), mAppName(nullptr) {} + ~nsDBusRemoteServer() override { Shutdown(); } + + nsresult Startup(const char* aAppName, const char* aProfileName) override; + void Shutdown() override; + + DBusHandlerResult HandleDBusMessage(DBusConnection* aConnection, + DBusMessage* msg); + void UnregisterDBusInterface(DBusConnection* aConnection); + + private: + DBusHandlerResult OpenURL(DBusMessage* msg); + DBusHandlerResult Introspect(DBusMessage* msg); + + // The connection is owned by DBus library + RefPtr<DBusConnection> mConnection; + nsCString mAppName; + nsCString mPathName; +}; + +#endif // __nsDBusRemoteServer_h__ diff --git a/toolkit/components/remote/nsGTKRemoteServer.cpp b/toolkit/components/remote/nsGTKRemoteServer.cpp new file mode 100644 index 0000000000..5b6eda3f89 --- /dev/null +++ b/toolkit/components/remote/nsGTKRemoteServer.cpp @@ -0,0 +1,69 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:expandtab:shiftwidth=2:tabstop=8: + */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsGTKRemoteServer.h" + +#include <gtk/gtk.h> +#include <gdk/gdk.h> +#include <gdk/gdkx.h> + +#include "mozilla/ModuleUtils.h" +#include "nsAppShellCID.h" + +#include "nsCOMPtr.h" + +#include "nsGTKToolkit.h" + +nsresult nsGTKRemoteServer::Startup(const char* aAppName, + const char* aProfileName) { + NS_ASSERTION(aAppName, "Don't pass a null appname!"); + + if (mServerWindow) { + return NS_ERROR_ALREADY_INITIALIZED; + } + + XRemoteBaseStartup(aAppName, aProfileName); + + mServerWindow = gtk_invisible_new(); + gtk_widget_realize(mServerWindow); + HandleCommandsFor(mServerWindow); + + return NS_OK; +} + +void nsGTKRemoteServer::Shutdown() { + if (!mServerWindow) { + return; + } + + gtk_widget_destroy(mServerWindow); + mServerWindow = nullptr; +} + +void nsGTKRemoteServer::HandleCommandsFor(GtkWidget* widget) { + g_signal_connect(G_OBJECT(widget), "property_notify_event", + G_CALLBACK(HandlePropertyChange), this); + + gtk_widget_add_events(widget, GDK_PROPERTY_CHANGE_MASK); + + Window window = gdk_x11_window_get_xid(gtk_widget_get_window(widget)); + nsXRemoteServer::HandleCommandsFor(window); +} + +gboolean nsGTKRemoteServer::HandlePropertyChange(GtkWidget* aWidget, + GdkEventProperty* pevent, + nsGTKRemoteServer* aThis) { + if (pevent->state == GDK_PROPERTY_NEW_VALUE) { + Atom changedAtom = gdk_x11_atom_to_xatom(pevent->atom); + + XID window = gdk_x11_window_get_xid(gtk_widget_get_window(aWidget)); + return aThis->HandleNewProperty( + window, GDK_DISPLAY_XDISPLAY(gdk_display_get_default()), pevent->time, + changedAtom); + } + return FALSE; +} diff --git a/toolkit/components/remote/nsGTKRemoteServer.h b/toolkit/components/remote/nsGTKRemoteServer.h new file mode 100644 index 0000000000..767f2f54ad --- /dev/null +++ b/toolkit/components/remote/nsGTKRemoteServer.h @@ -0,0 +1,37 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:expandtab:shiftwidth=2:tabstop=2: + */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef __nsGTKRemoteServer_h__ +#define __nsGTKRemoteServer_h__ + +#include <gdk/gdk.h> +#include <gdk/gdkx.h> +#include <gtk/gtk.h> + +#include "nsRemoteServer.h" +#include "nsXRemoteServer.h" +#include "mozilla/Attributes.h" + +class nsGTKRemoteServer final : public nsXRemoteServer { + public: + nsGTKRemoteServer() : mServerWindow(nullptr) {} + ~nsGTKRemoteServer() override { Shutdown(); } + + nsresult Startup(const char* aAppName, const char* aProfileName) override; + void Shutdown() override; + + static gboolean HandlePropertyChange(GtkWidget* widget, + GdkEventProperty* event, + nsGTKRemoteServer* aData); + + private: + void HandleCommandsFor(GtkWidget* aWidget); + + GtkWidget* mServerWindow; +}; + +#endif // __nsGTKRemoteService_h__ diff --git a/toolkit/components/remote/nsMacRemoteClient.h b/toolkit/components/remote/nsMacRemoteClient.h new file mode 100644 index 0000000000..96ad4e2a7f --- /dev/null +++ b/toolkit/components/remote/nsMacRemoteClient.h @@ -0,0 +1,27 @@ +/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:expandtab:shiftwidth=4:tabstop=4: + */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef TOOLKIT_COMPONENTS_REMOTE_NSMACREMOTECLIENT_H_ +#define TOOLKIT_COMPONENTS_REMOTE_NSMACREMOTECLIENT_H_ + +#import <CoreFoundation/CoreFoundation.h> + +#include "nscore.h" +#include "nsRemoteClient.h" + +class nsMacRemoteClient : public nsRemoteClient { + public: + virtual ~nsMacRemoteClient() = default; + + nsresult Init() override; + + nsresult SendCommandLine(const char* aProgram, const char* aProfile, + int32_t argc, char** argv, const char* aStartupToken, + char** aResponse, bool* aSucceeded) override; +}; + +#endif // TOOLKIT_COMPONENTS_REMOTE_NSMACREMOTECLIENT_H_ diff --git a/toolkit/components/remote/nsMacRemoteClient.mm b/toolkit/components/remote/nsMacRemoteClient.mm new file mode 100644 index 0000000000..314eebc030 --- /dev/null +++ b/toolkit/components/remote/nsMacRemoteClient.mm @@ -0,0 +1,62 @@ +/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:expandtab:shiftwidth=4:tabstop=4: + */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#import <CoreFoundation/CoreFoundation.h> +#import <Foundation/Foundation.h> + +#include <sys/param.h> + +#include "MacAutoreleasePool.h" +#include "nsMacRemoteClient.h" +#include "RemoteUtils.h" + +using namespace mozilla; + +nsresult nsMacRemoteClient::Init() { return NS_OK; } + +nsresult nsMacRemoteClient::SendCommandLine(const char* aProgram, const char* aProfile, + int32_t argc, char** argv, + const char* aDesktopStartupID, char** aResponse, + bool* aSucceeded) { + mozilla::MacAutoreleasePool pool; + + *aSucceeded = false; + + nsString className; + BuildClassName(aProgram, aProfile, className); + NSString* serverNameString = + [NSString stringWithCharacters:reinterpret_cast<const unichar*>(className.get()) + length:className.Length()]; + + CFMessagePortRef messageServer = CFMessagePortCreateRemote(0, (CFStringRef)serverNameString); + + if (messageServer) { + // Getting current process directory + char cwdPtr[MAXPATHLEN + 1]; + getcwd(cwdPtr, MAXPATHLEN + 1); + + NSMutableArray* argumentsArray = [NSMutableArray array]; + for (int i = 0; i < argc; i++) { + NSString* argument = [NSString stringWithUTF8String:argv[i]]; + [argumentsArray addObject:argument]; + } + NSDictionary* dict = @{@"args" : argumentsArray}; + + NSData* data = [NSKeyedArchiver archivedDataWithRootObject:dict]; + + CFMessagePortSendRequest(messageServer, 0, (CFDataRef)data, 10.0, 0.0, NULL, NULL); + + CFMessagePortInvalidate(messageServer); + CFRelease(messageServer); + *aSucceeded = true; + + } else { + // Remote Server not found. Doing nothing. + } + + return NS_OK; +} diff --git a/toolkit/components/remote/nsMacRemoteServer.h b/toolkit/components/remote/nsMacRemoteServer.h new file mode 100644 index 0000000000..0e2bda8a32 --- /dev/null +++ b/toolkit/components/remote/nsMacRemoteServer.h @@ -0,0 +1,30 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:expandtab:shiftwidth=2:tabstop=2: + */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef TOOLKIT_COMPONENTS_REMOTE_NSMACREMOTESERVER_H_ +#define TOOLKIT_COMPONENTS_REMOTE_NSMACREMOTESERVER_H_ + +#import <CoreFoundation/CoreFoundation.h> + +#include "nsRemoteServer.h" + +class nsMacRemoteServer final : public nsRemoteServer { + public: + nsMacRemoteServer() = default; + ~nsMacRemoteServer() override { Shutdown(); } + + nsresult Startup(const char* aAppName, const char* aProfileName) override; + void Shutdown() override; + + void HandleCommandLine(CFDataRef aData); + + private: + CFRunLoopSourceRef mRunLoopSource; + CFMessagePortRef mMessageServer; +}; + +#endif // TOOLKIT_COMPONENTS_REMOTE_NSMACREMOTESERVER_H_ diff --git a/toolkit/components/remote/nsMacRemoteServer.mm b/toolkit/components/remote/nsMacRemoteServer.mm new file mode 100644 index 0000000000..0936d4d2fd --- /dev/null +++ b/toolkit/components/remote/nsMacRemoteServer.mm @@ -0,0 +1,134 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:expandtab:shiftwidth=2:tabstop=2: + */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#import <Cocoa/Cocoa.h> +#import <CoreServices/CoreServices.h> + +#include "MacAutoreleasePool.h" +#include "nsCOMPtr.h" +#include "nsIComponentManager.h" +#include "nsIServiceManager.h" +#include "nsIWindowMediator.h" +#include "nsIWidget.h" +#include "nsICommandLineRunner.h" +#include "nsICommandLine.h" +#include "nsCommandLine.h" +#include "nsIDocShell.h" +#include "nsMacRemoteServer.h" +#include "nsXPCOM.h" +#include "RemoteUtils.h" + +CFDataRef messageServerCallback(CFMessagePortRef aLocal, int32_t aMsgid, CFDataRef aData, + void* aInfo) { + // One of the clients submitted a structure. + static_cast<nsMacRemoteServer*>(aInfo)->HandleCommandLine(aData); + + return NULL; +} + +// aData contains serialized Dictionary, which in turn contains command line arguments +void nsMacRemoteServer::HandleCommandLine(CFDataRef aData) { + mozilla::MacAutoreleasePool pool; + + if (aData) { + NSDictionary* dict = [NSKeyedUnarchiver unarchiveObjectWithData:(NSData*)aData]; + if (dict && [dict isKindOfClass:[NSDictionary class]]) { + NSArray* args = dict[@"args"]; + if (!args) { + NS_ERROR("Wrong parameters passed to the Remote Server"); + return; + } + + nsCOMPtr<nsICommandLineRunner> cmdLine(new nsCommandLine()); + + // Converting Objective-C array into a C array, + // which nsICommandLineRunner understands. + int argc = [args count]; + const char** argv = new const char*[argc]; + for (int i = 0; i < argc; i++) { + const char* arg = [[args objectAtIndex:i] UTF8String]; + argv[i] = arg; + } + + nsresult rv = cmdLine->Init(argc, argv, nullptr, nsICommandLine::STATE_REMOTE_AUTO); + + // Cleaning up C array. + delete[] argv; + + if (NS_FAILED(rv)) { + NS_ERROR("Error initializing command line."); + return; + } + + // Processing the command line, passed from a remote instance + // in the current instance. + cmdLine->Run(); + + // And bring the app's window to front. + [[NSRunningApplication currentApplication] + activateWithOptions:NSApplicationActivateIgnoringOtherApps]; + } + } +} + +nsresult nsMacRemoteServer::Startup(const char* aAppName, const char* aProfileName) { + // This is the first instance ever. + // Let's register a notification listener here, + // In case future instances would want to notify us about command line arguments + // passed to them. Note, that if mozilla process is restarting, we still need to + // register for notifications. + + mozilla::MacAutoreleasePool pool; + + nsString className; + BuildClassName(aAppName, aProfileName, className); + + NSString* serverNameString = + [NSString stringWithCharacters:reinterpret_cast<const unichar*>(className.get()) + length:className.Length()]; + + CFMessagePortContext context; + context.copyDescription = NULL; + context.info = this; + context.release = NULL; + context.retain = NULL; + context.version = NULL; + mMessageServer = CFMessagePortCreateLocal(NULL, (CFStringRef)serverNameString, + messageServerCallback, &context, NULL); + if (!mMessageServer) { + return NS_ERROR_FAILURE; + } + mRunLoopSource = CFMessagePortCreateRunLoopSource(NULL, mMessageServer, 0); + if (!mRunLoopSource) { + CFRelease(mMessageServer); + mMessageServer = NULL; + return NS_ERROR_FAILURE; + } + CFRunLoopRef runLoop = CFRunLoopGetMain(); + CFRunLoopAddSource(runLoop, mRunLoopSource, kCFRunLoopDefaultMode); + + return NS_OK; +} + +void nsMacRemoteServer::Shutdown() { + // 1) Invalidate server connection + if (mMessageServer) { + CFMessagePortInvalidate(mMessageServer); + } + + // 2) Release run loop source + if (mRunLoopSource) { + CFRelease(mRunLoopSource); + mRunLoopSource = NULL; + } + + // 3) Release server connection + if (mMessageServer) { + CFRelease(mMessageServer); + mMessageServer = NULL; + } +} diff --git a/toolkit/components/remote/nsRemoteClient.h b/toolkit/components/remote/nsRemoteClient.h new file mode 100644 index 0000000000..f36c33287b --- /dev/null +++ b/toolkit/components/remote/nsRemoteClient.h @@ -0,0 +1,64 @@ +/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:expandtab:shiftwidth=4:tabstop=4: + */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef TOOLKIT_COMPONENTS_REMOTE_NSREMOTECLIENT_H_ +#define TOOLKIT_COMPONENTS_REMOTE_NSREMOTECLIENT_H_ + +#include "nscore.h" + +/** + * Pure-virtual common base class for remoting implementations. + */ + +class nsRemoteClient { + public: + virtual ~nsRemoteClient() = default; + + /** + * Initializes the client + */ + virtual nsresult Init() = 0; + + /** + * Send a complete command line to a running instance. + * + * @param aProgram This is the preferred program that we want to use + * for this particular command. + * + * @param aUsername This allows someone to only talk to an instance + * of the server that's running under a particular username. If + * this isn't specified here it's pulled from the LOGNAME + * environmental variable if it's set. + * + * @param aProfile This allows you to specify a particular server + * running under a named profile. If it is not specified the + * profile is not checked. + * + * @param argc The number of command-line arguments. + * + * @param argv The command-line arguments. + * + * @param aStartupToken the contents of the DESKTOP_STARTUP_ID environment + * variable defined by the Startup Notification specification, or the + * XDG_ACTIVATION_TOKEN defined by Wayland. + * http://standards.freedesktop.org/startup-notification-spec/startup-notification-0.1.txt + * https://wayland.app/protocols/xdg-activation-v1 + * + * @param aResponse If there is a response, it will be here. This + * includes error messages. The string is allocated using stdlib + * string functions, so free it with free(). + * + * @return true if succeeded, false if no running instance was found. + * + */ + virtual nsresult SendCommandLine(const char* aProgram, const char* aProfile, + int32_t argc, char** argv, + const char* aStartupToken, char** aResponse, + bool* aSucceeded) = 0; +}; + +#endif // TOOLKIT_COMPONENTS_REMOTE_NSREMOTECLIENT_H_ diff --git a/toolkit/components/remote/nsRemoteServer.h b/toolkit/components/remote/nsRemoteServer.h new file mode 100644 index 0000000000..7c24bdce71 --- /dev/null +++ b/toolkit/components/remote/nsRemoteServer.h @@ -0,0 +1,21 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:expandtab:shiftwidth=2:tabstop=2: + */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef __nsRemoteServer_h__ +#define __nsRemoteServer_h__ + +#include "nsString.h" + +class nsRemoteServer { + public: + virtual ~nsRemoteServer() = default; + + virtual nsresult Startup(const char* aAppName, const char* aProfileName) = 0; + virtual void Shutdown() = 0; +}; + +#endif // __nsRemoteServer_h__ diff --git a/toolkit/components/remote/nsRemoteService.cpp b/toolkit/components/remote/nsRemoteService.cpp new file mode 100644 index 0000000000..3e8e4b5cb8 --- /dev/null +++ b/toolkit/components/remote/nsRemoteService.cpp @@ -0,0 +1,192 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:expandtab:shiftwidth=2:tabstop=8: + */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifdef MOZ_WIDGET_GTK +# ifdef MOZ_ENABLE_DBUS +# include "nsDBusRemoteServer.h" +# include "nsDBusRemoteClient.h" +# else +# include "nsGTKRemoteServer.h" +# include "nsXRemoteClient.h" +# endif +#elif defined(XP_WIN) +# include "nsWinRemoteServer.h" +# include "nsWinRemoteClient.h" +#elif defined(XP_DARWIN) +# include "nsMacRemoteServer.h" +# include "nsMacRemoteClient.h" +#endif +#include "nsRemoteService.h" + +#include "nsIObserverService.h" +#include "nsString.h" +#include "nsServiceManagerUtils.h" +#include "SpecialSystemDirectory.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/UniquePtr.h" + +// Time to wait for the remoting service to start +#define START_TIMEOUT_SEC 5 +#define START_SLEEP_MSEC 100 + +using namespace mozilla; + +extern int gArgc; +extern char** gArgv; + +using namespace mozilla; + +NS_IMPL_ISUPPORTS(nsRemoteService, nsIObserver) + +nsRemoteService::nsRemoteService(const char* aProgram) : mProgram(aProgram) { + ToLowerCase(mProgram); +} + +void nsRemoteService::SetProfile(nsACString& aProfile) { mProfile = aProfile; } + +void nsRemoteService::LockStartup() { + nsCOMPtr<nsIFile> mutexDir; + nsresult rv = GetSpecialSystemDirectory(OS_TemporaryDirectory, + getter_AddRefs(mutexDir)); + NS_ENSURE_SUCCESS_VOID(rv); + rv = mutexDir->AppendNative(mProgram); + NS_ENSURE_SUCCESS_VOID(rv); + + const mozilla::TimeStamp epoch = mozilla::TimeStamp::Now(); + do { + // If we have been waiting for another instance to release the lock it will + // have deleted the lock directory when doing so we have to make sure it + // exists every time we poll for the lock. + rv = mutexDir->Create(nsIFile::DIRECTORY_TYPE, 0700); + if (NS_SUCCEEDED(rv) || rv == NS_ERROR_FILE_ALREADY_EXISTS) { + mRemoteLockDir = mutexDir; + } else { + NS_WARNING("Unable to create startup lock directory."); + return; + } + + rv = mRemoteLock.Lock(mRemoteLockDir, nullptr); + if (NS_SUCCEEDED(rv)) { + return; + } + + mRemoteLockDir = nullptr; + PR_Sleep(START_SLEEP_MSEC); + } while ((mozilla::TimeStamp::Now() - epoch) < + mozilla::TimeDuration::FromSeconds(START_TIMEOUT_SEC)); + + NS_WARNING("Failed to lock for startup, continuing anyway."); +} + +void nsRemoteService::UnlockStartup() { + if (mRemoteLockDir) { + mRemoteLock.Unlock(); + mRemoteLock.Cleanup(); + + mRemoteLockDir->Remove(false); + mRemoteLockDir = nullptr; + } +} + +RemoteResult nsRemoteService::StartClient(const char* aStartupToken) { + if (mProfile.IsEmpty()) { + return REMOTE_NOT_FOUND; + } + + UniquePtr<nsRemoteClient> client; +#ifdef MOZ_WIDGET_GTK +# if defined(MOZ_ENABLE_DBUS) + client = MakeUnique<nsDBusRemoteClient>(); +# else + client = MakeUnique<nsXRemoteClient>(); +# endif +#elif defined(XP_WIN) + client = MakeUnique<nsWinRemoteClient>(); +#elif defined(XP_DARWIN) + client = MakeUnique<nsMacRemoteClient>(); +#else + return REMOTE_NOT_FOUND; +#endif + + nsresult rv = client ? client->Init() : NS_ERROR_FAILURE; + if (NS_FAILED(rv)) return REMOTE_NOT_FOUND; + + nsCString response; + bool success = false; + rv = + client->SendCommandLine(mProgram.get(), mProfile.get(), gArgc, gArgv, + aStartupToken, getter_Copies(response), &success); + // did the command fail? + if (!success) return REMOTE_NOT_FOUND; + + // The "command not parseable" error is returned when the + // nsICommandLineHandler throws a NS_ERROR_ABORT. + if (response.EqualsLiteral("500 command not parseable")) + return REMOTE_ARG_BAD; + + if (NS_FAILED(rv)) return REMOTE_NOT_FOUND; + + return REMOTE_FOUND; +} + +void nsRemoteService::StartupServer() { + if (mRemoteServer) { + return; + } + + if (mProfile.IsEmpty()) { + return; + } + +#ifdef MOZ_WIDGET_GTK +# if defined(MOZ_ENABLE_DBUS) + mRemoteServer = MakeUnique<nsDBusRemoteServer>(); +# else + mRemoteServer = MakeUnique<nsGTKRemoteServer>(); +# endif +#elif defined(XP_WIN) + mRemoteServer = MakeUnique<nsWinRemoteServer>(); +#elif defined(XP_DARWIN) + mRemoteServer = MakeUnique<nsMacRemoteServer>(); +#else + return; +#endif + + if (!mRemoteServer) { + return; + } + + nsresult rv = mRemoteServer->Startup(mProgram.get(), mProfile.get()); + + if (NS_FAILED(rv)) { + mRemoteServer = nullptr; + return; + } + + nsCOMPtr<nsIObserverService> obs( + do_GetService("@mozilla.org/observer-service;1")); + if (obs) { + obs->AddObserver(this, "xpcom-shutdown", false); + obs->AddObserver(this, "quit-application", false); + } +} + +void nsRemoteService::ShutdownServer() { mRemoteServer = nullptr; } + +nsRemoteService::~nsRemoteService() { + UnlockStartup(); + ShutdownServer(); +} + +NS_IMETHODIMP +nsRemoteService::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + // This can be xpcom-shutdown or quit-application, but it's the same either + // way. + ShutdownServer(); + return NS_OK; +} diff --git a/toolkit/components/remote/nsRemoteService.h b/toolkit/components/remote/nsRemoteService.h new file mode 100644 index 0000000000..3105913a7f --- /dev/null +++ b/toolkit/components/remote/nsRemoteService.h @@ -0,0 +1,49 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:expandtab:shiftwidth=2:tabstop=2: + */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef TOOLKIT_COMPONENTS_REMOTE_NSREMOTESERVER_H_ +#define TOOLKIT_COMPONENTS_REMOTE_NSREMOTESERVER_H_ + +#include "nsRemoteServer.h" +#include "nsIObserver.h" +#include "mozilla/UniquePtr.h" +#include "nsIFile.h" +#include "nsProfileLock.h" + +enum RemoteResult { + REMOTE_NOT_FOUND = 0, + REMOTE_FOUND = 1, + REMOTE_ARG_BAD = 2 +}; + +class nsRemoteService final : public nsIObserver { + public: + // We will be a static singleton, so don't use the ordinary methods. + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER + + explicit nsRemoteService(const char* aProgram); + void SetProfile(nsACString& aProfile); + + void LockStartup(); + void UnlockStartup(); + + RemoteResult StartClient(const char* aStartupToken); + void StartupServer(); + void ShutdownServer(); + + private: + ~nsRemoteService(); + + mozilla::UniquePtr<nsRemoteServer> mRemoteServer; + nsProfileLock mRemoteLock; + nsCOMPtr<nsIFile> mRemoteLockDir; + nsCString mProgram; + nsCString mProfile; +}; + +#endif // TOOLKIT_COMPONENTS_REMOTE_NSREMOTESERVER_H_ diff --git a/toolkit/components/remote/nsUnixRemoteServer.cpp b/toolkit/components/remote/nsUnixRemoteServer.cpp new file mode 100644 index 0000000000..f26c90af4a --- /dev/null +++ b/toolkit/components/remote/nsUnixRemoteServer.cpp @@ -0,0 +1,113 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:expandtab:shiftwidth=2:tabstop=2: + */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsUnixRemoteServer.h" +#include "nsGTKToolkit.h" +#include "nsCOMPtr.h" +#include "nsICommandLineRunner.h" +#include "nsCommandLine.h" +#include "nsIFile.h" + +// Set desktop startup ID to the passed ID, if there is one, so that any created +// windows get created with the right window manager metadata, and any windows +// that get new tabs and are activated also get the right WM metadata. +// The timestamp will be used if there is no desktop startup ID, or if we're +// raising an existing window rather than showing a new window for the first +// time. +void nsUnixRemoteServer::SetStartupTokenOrTimestamp( + const nsACString& aStartupToken, uint32_t aTimestamp) { + nsGTKToolkit* toolkit = nsGTKToolkit::GetToolkit(); + if (!toolkit) { + return; + } + + if (!aStartupToken.IsEmpty()) { + toolkit->SetStartupToken(aStartupToken); + } + + toolkit->SetFocusTimestamp(aTimestamp); +} + +static bool FindExtensionParameterInCommand(const char* aParameterName, + const nsACString& aCommand, + char aSeparator, + nsACString* aValue) { + nsAutoCString searchFor; + searchFor.Append(aSeparator); + searchFor.Append(aParameterName); + searchFor.Append('='); + + nsACString::const_iterator start, end; + aCommand.BeginReading(start); + aCommand.EndReading(end); + if (!FindInReadable(searchFor, start, end)) return false; + + nsACString::const_iterator charStart, charEnd; + charStart = end; + aCommand.EndReading(charEnd); + nsACString::const_iterator idStart = charStart, idEnd; + if (FindCharInReadable(aSeparator, charStart, charEnd)) { + idEnd = charStart; + } else { + idEnd = charEnd; + } + *aValue = nsDependentCSubstring(idStart, idEnd); + return true; +} + +const char* nsUnixRemoteServer::HandleCommandLine(const char* aBuffer, + uint32_t aTimestamp) { + nsCOMPtr<nsICommandLineRunner> cmdline(new nsCommandLine()); + + // the commandline property is constructed as an array of int32_t + // followed by a series of null-terminated strings: + // + // [argc][offsetargv0][offsetargv1...]<workingdir>\0<argv[0]>\0argv[1]...\0 + // (offset is from the beginning of the buffer) + + int32_t argc = TO_LITTLE_ENDIAN32(*reinterpret_cast<const int32_t*>(aBuffer)); + const char* wd = aBuffer + ((argc + 1) * sizeof(int32_t)); + + nsCOMPtr<nsIFile> lf; + nsresult rv = + NS_NewNativeLocalFile(nsDependentCString(wd), true, getter_AddRefs(lf)); + if (NS_FAILED(rv)) return "509 internal error"; + + nsAutoCString desktopStartupID; + + const char** argv = (const char**)malloc(sizeof(char*) * argc); + if (!argv) return "509 internal error"; + + const int32_t* offset = reinterpret_cast<const int32_t*>(aBuffer) + 1; + + for (int i = 0; i < argc; ++i) { + argv[i] = aBuffer + TO_LITTLE_ENDIAN32(offset[i]); + + if (i == 0) { + nsDependentCString cmd(argv[0]); + FindExtensionParameterInCommand("STARTUP_TOKEN", cmd, ' ', + &desktopStartupID); + } + } + + rv = cmdline->Init(argc, argv, lf, nsICommandLine::STATE_REMOTE_AUTO); + + free(argv); + if (NS_FAILED(rv)) { + return "509 internal error"; + } + + SetStartupTokenOrTimestamp(desktopStartupID, aTimestamp); + + rv = cmdline->Run(); + + if (NS_ERROR_ABORT == rv) return "500 command not parseable"; + + if (NS_FAILED(rv)) return "509 internal error"; + + return "200 executed command"; +} diff --git a/toolkit/components/remote/nsUnixRemoteServer.h b/toolkit/components/remote/nsUnixRemoteServer.h new file mode 100644 index 0000000000..003f2efbf8 --- /dev/null +++ b/toolkit/components/remote/nsUnixRemoteServer.h @@ -0,0 +1,28 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:expandtab:shiftwidth=2:tabstop=2: + */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef __nsUnixRemoteServer_h__ +#define __nsUnixRemoteServer_h__ + +#include "nsStringFwd.h" + +#ifdef IS_BIG_ENDIAN +# define TO_LITTLE_ENDIAN32(x) \ + ((((x)&0xff000000) >> 24) | (((x)&0x00ff0000) >> 8) | \ + (((x)&0x0000ff00) << 8) | (((x)&0x000000ff) << 24)) +#else +# define TO_LITTLE_ENDIAN32(x) (x) +#endif + +class nsUnixRemoteServer { + protected: + void SetStartupTokenOrTimestamp(const nsACString& aStartupToken, + uint32_t aTimestamp); + const char* HandleCommandLine(const char* aBuffer, uint32_t aTimestamp); +}; + +#endif // __nsGTKRemoteService_h__ diff --git a/toolkit/components/remote/nsWinRemoteClient.cpp b/toolkit/components/remote/nsWinRemoteClient.cpp new file mode 100644 index 0000000000..4428070bfa --- /dev/null +++ b/toolkit/components/remote/nsWinRemoteClient.cpp @@ -0,0 +1,44 @@ +/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:expandtab:shiftwidth=4:tabstop=4: + */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsWinRemoteClient.h" +#include <windows.h> +#include "RemoteUtils.h" +#include "WinRemoteMessage.h" + +using namespace mozilla; + +nsresult nsWinRemoteClient::Init() { return NS_OK; } + +nsresult nsWinRemoteClient::SendCommandLine( + const char* aProgram, const char* aProfile, int32_t argc, char** argv, + const char* aStartupToken, char** aResponse, bool* aSucceeded) { + *aSucceeded = false; + + nsString className; + BuildClassName(aProgram, aProfile, className); + + HWND handle = ::FindWindowW(className.get(), 0); + + if (!handle) { + return NS_OK; + } + + WCHAR cwd[MAX_PATH]; + _wgetcwd(cwd, MAX_PATH); + WinRemoteMessageSender sender(::GetCommandLineW(), cwd); + + // Bring the already running Mozilla process to the foreground. + // nsWindow will restore the window (if minimized) and raise it. + ::SetForegroundWindow(handle); + ::SendMessageW(handle, WM_COPYDATA, 0, + reinterpret_cast<LPARAM>(sender.CopyData())); + + *aSucceeded = true; + + return NS_OK; +} diff --git a/toolkit/components/remote/nsWinRemoteClient.h b/toolkit/components/remote/nsWinRemoteClient.h new file mode 100644 index 0000000000..64859b69a1 --- /dev/null +++ b/toolkit/components/remote/nsWinRemoteClient.h @@ -0,0 +1,25 @@ +/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:expandtab:shiftwidth=4:tabstop=4: + */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsWinRemoteClient_h__ +#define nsWinRemoteClient_h__ + +#include "nscore.h" +#include "nsRemoteClient.h" + +class nsWinRemoteClient : public nsRemoteClient { + public: + virtual ~nsWinRemoteClient() = default; + + nsresult Init() override; + + nsresult SendCommandLine(const char* aProgram, const char* aProfile, + int32_t argc, char** argv, const char* aStartupToken, + char** aResponse, bool* aSucceeded) override; +}; + +#endif // nsWinRemoteClient_h__ diff --git a/toolkit/components/remote/nsWinRemoteServer.cpp b/toolkit/components/remote/nsWinRemoteServer.cpp new file mode 100644 index 0000000000..0fa4f5facc --- /dev/null +++ b/toolkit/components/remote/nsWinRemoteServer.cpp @@ -0,0 +1,104 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:expandtab:shiftwidth=2:tabstop=2: + */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "CmdLineAndEnvUtils.h" +#include "nsWinRemoteServer.h" +#include "RemoteUtils.h" +#include "nsCOMPtr.h" +#include "nsXPCOM.h" +#include "nsPIDOMWindow.h" +#include "nsIWindowMediator.h" +#include "nsIBaseWindow.h" +#include "nsIWidget.h" +#include "nsICommandLineRunner.h" +#include "nsICommandLine.h" +#include "nsCommandLine.h" +#include "nsIDocShell.h" +#include "WinRemoteMessage.h" + +HWND hwndForDOMWindow(mozIDOMWindowProxy* window) { + if (!window) { + return 0; + } + nsCOMPtr<nsPIDOMWindowOuter> pidomwindow = nsPIDOMWindowOuter::From(window); + + nsCOMPtr<nsIBaseWindow> ppBaseWindow = + do_QueryInterface(pidomwindow->GetDocShell()); + if (!ppBaseWindow) { + return 0; + } + + nsCOMPtr<nsIWidget> ppWidget; + ppBaseWindow->GetMainWidget(getter_AddRefs(ppWidget)); + + return (HWND)(ppWidget->GetNativeData(NS_NATIVE_WIDGET)); +} + +static nsresult GetMostRecentWindow(mozIDOMWindowProxy** aWindow) { + nsresult rv; + nsCOMPtr<nsIWindowMediator> med( + do_GetService(NS_WINDOWMEDIATOR_CONTRACTID, &rv)); + if (NS_FAILED(rv)) return rv; + + if (med) return med->GetMostRecentWindow(nullptr, aWindow); + + return NS_ERROR_FAILURE; +} + +LRESULT CALLBACK WindowProc(HWND msgWindow, UINT msg, WPARAM wp, LPARAM lp) { + if (msg == WM_COPYDATA) { + WinRemoteMessageReceiver receiver; + if (NS_SUCCEEDED(receiver.Parse(reinterpret_cast<COPYDATASTRUCT*>(lp)))) { + receiver.CommandLineRunner()->Run(); + } else { + NS_ERROR("Error initializing command line."); + } + + // Get current window and return its window handle. + nsCOMPtr<mozIDOMWindowProxy> win; + GetMostRecentWindow(getter_AddRefs(win)); + return win ? (LRESULT)hwndForDOMWindow(win) : 0; + } + return DefWindowProcW(msgWindow, msg, wp, lp); +} + +nsresult nsWinRemoteServer::Startup(const char* aAppName, + const char* aProfileName) { + nsString className; + BuildClassName(aAppName, aProfileName, className); + + WNDCLASSW classStruct = {0, // style + &WindowProc, // lpfnWndProc + 0, // cbClsExtra + 0, // cbWndExtra + 0, // hInstance + 0, // hIcon + 0, // hCursor + 0, // hbrBackground + 0, // lpszMenuName + className.get()}; // lpszClassName + + // Register the window class. + NS_ENSURE_TRUE(::RegisterClassW(&classStruct), NS_ERROR_FAILURE); + + // Create the window. + mHandle = ::CreateWindowW(className.get(), + 0, // title + WS_CAPTION, // style + 0, 0, 0, 0, // x, y, cx, cy + 0, // parent + 0, // menu + 0, // instance + 0); // create struct + + return mHandle ? NS_OK : NS_ERROR_FAILURE; +} + +void nsWinRemoteServer::Shutdown() { + DestroyWindow(mHandle); + mHandle = nullptr; +} diff --git a/toolkit/components/remote/nsWinRemoteServer.h b/toolkit/components/remote/nsWinRemoteServer.h new file mode 100644 index 0000000000..1687e2d44b --- /dev/null +++ b/toolkit/components/remote/nsWinRemoteServer.h @@ -0,0 +1,27 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:expandtab:shiftwidth=2:tabstop=2: + */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef __nsWinRemoteServer_h__ +#define __nsWinRemoteServer_h__ + +#include "nsRemoteServer.h" + +#include <windows.h> + +class nsWinRemoteServer final : public nsRemoteServer { + public: + nsWinRemoteServer() = default; + ~nsWinRemoteServer() override { Shutdown(); } + + nsresult Startup(const char* aAppName, const char* aProfileName) override; + void Shutdown() override; + + private: + HWND mHandle; +}; + +#endif // __nsWinRemoteService_h__ diff --git a/toolkit/components/remote/nsXRemoteClient.cpp b/toolkit/components/remote/nsXRemoteClient.cpp new file mode 100644 index 0000000000..198f7f70c2 --- /dev/null +++ b/toolkit/components/remote/nsXRemoteClient.cpp @@ -0,0 +1,653 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:expandtab:shiftwidth=2:tabstop=8: + */ +/* vim:set ts=8 sw=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsDebug.h" +#include "mozilla/ArrayUtils.h" +#include "mozilla/IntegerPrintfMacros.h" +#include "mozilla/Sprintf.h" +#include "mozilla/Unused.h" +#include "nsXRemoteClient.h" +#include "RemoteUtils.h" +#include "prsystem.h" +#include "mozilla/Logging.h" +#include "prenv.h" +#include "prdtoa.h" +#include <X11/Xatom.h> +#include <limits.h> +#include <poll.h> +#include <stdlib.h> +#include <string.h> +#include <strings.h> +#include <sys/time.h> +#include <sys/types.h> +#include <unistd.h> + +#define MOZILLA_VERSION_PROP "_MOZILLA_VERSION" +#define MOZILLA_LOCK_PROP "_MOZILLA_LOCK" +#define MOZILLA_COMMANDLINE_PROP "_MOZILLA_COMMANDLINE" +#define MOZILLA_RESPONSE_PROP "_MOZILLA_RESPONSE" +#define MOZILLA_USER_PROP "_MOZILLA_USER" +#define MOZILLA_PROFILE_PROP "_MOZILLA_PROFILE" +#define MOZILLA_PROGRAM_PROP "_MOZILLA_PROGRAM" + +#ifdef IS_BIG_ENDIAN +# define TO_LITTLE_ENDIAN32(x) \ + ((((x)&0xff000000) >> 24) | (((x)&0x00ff0000) >> 8) | \ + (((x)&0x0000ff00) << 8) | (((x)&0x000000ff) << 24)) +#else +# define TO_LITTLE_ENDIAN32(x) (x) +#endif + +#ifndef MAX_PATH +# ifdef PATH_MAX +# define MAX_PATH PATH_MAX +# else +# define MAX_PATH 1024 +# endif +#endif + +using mozilla::LogLevel; +using mozilla::Unused; + +static mozilla::LazyLogModule sRemoteLm("nsXRemoteClient"); + +static int (*sOldHandler)(Display*, XErrorEvent*); +static bool sGotBadWindow; + +nsXRemoteClient::nsXRemoteClient() { + mDisplay = 0; + mInitialized = false; + mMozVersionAtom = 0; + mMozLockAtom = 0; + mMozCommandLineAtom = 0; + mMozResponseAtom = 0; + mMozWMStateAtom = 0; + mMozUserAtom = 0; + mMozProfileAtom = 0; + mMozProgramAtom = 0; + mLockData = 0; + MOZ_LOG(sRemoteLm, LogLevel::Debug, ("nsXRemoteClient::nsXRemoteClient")); +} + +nsXRemoteClient::~nsXRemoteClient() { + MOZ_LOG(sRemoteLm, LogLevel::Debug, ("nsXRemoteClient::~nsXRemoteClient")); + if (mInitialized) Shutdown(); +} + +// Minimize the roundtrips to the X-server +static const char* XAtomNames[] = { + MOZILLA_VERSION_PROP, MOZILLA_LOCK_PROP, MOZILLA_RESPONSE_PROP, + "WM_STATE", MOZILLA_USER_PROP, MOZILLA_PROFILE_PROP, + MOZILLA_PROGRAM_PROP, MOZILLA_COMMANDLINE_PROP}; +static Atom XAtoms[MOZ_ARRAY_LENGTH(XAtomNames)]; + +nsresult nsXRemoteClient::Init() { + MOZ_LOG(sRemoteLm, LogLevel::Debug, ("nsXRemoteClient::Init")); + + if (mInitialized) return NS_OK; + + // try to open the display + mDisplay = XOpenDisplay(0); + if (!mDisplay) return NS_ERROR_FAILURE; + + // get our atoms + XInternAtoms(mDisplay, const_cast<char**>(XAtomNames), + MOZ_ARRAY_LENGTH(XAtomNames), False, XAtoms); + + int i = 0; + mMozVersionAtom = XAtoms[i++]; + mMozLockAtom = XAtoms[i++]; + mMozResponseAtom = XAtoms[i++]; + mMozWMStateAtom = XAtoms[i++]; + mMozUserAtom = XAtoms[i++]; + mMozProfileAtom = XAtoms[i++]; + mMozProgramAtom = XAtoms[i++]; + mMozCommandLineAtom = XAtoms[i]; + + mInitialized = true; + + return NS_OK; +} + +void nsXRemoteClient::Shutdown(void) { + MOZ_LOG(sRemoteLm, LogLevel::Debug, ("nsXRemoteClient::Shutdown")); + + if (!mInitialized) return; + + // shut everything down + XCloseDisplay(mDisplay); + mDisplay = 0; + mInitialized = false; + if (mLockData) { + free(mLockData); + mLockData = 0; + } +} + +static int HandleBadWindow(Display* display, XErrorEvent* event) { + if (event->error_code == BadWindow) { + sGotBadWindow = true; + return 0; // ignored + } + + return (*sOldHandler)(display, event); +} + +nsresult nsXRemoteClient::SendCommandLine( + const char* aProgram, const char* aProfile, int32_t argc, char** argv, + const char* aStartupToken, char** aResponse, bool* aWindowFound) { + NS_ENSURE_TRUE(aProgram, NS_ERROR_INVALID_ARG); + + MOZ_LOG(sRemoteLm, LogLevel::Debug, ("nsXRemoteClient::SendCommandLine")); + + *aWindowFound = false; + + // FindBestWindow() iterates down the window hierarchy, so catch X errors + // when windows get destroyed before being accessed. + sOldHandler = XSetErrorHandler(HandleBadWindow); + + Window w = FindBestWindow(aProgram, aProfile); + + nsresult rv = NS_OK; + + if (w) { + // ok, let the caller know that we at least found a window. + *aWindowFound = true; + + // Ignore BadWindow errors up to this point. The last request from + // FindBestWindow() was a synchronous XGetWindowProperty(), so no need to + // Sync. Leave the error handler installed to detect if w gets destroyed. + sGotBadWindow = false; + + // make sure we get the right events on that window + XSelectInput(mDisplay, w, (PropertyChangeMask | StructureNotifyMask)); + + bool destroyed = false; + + // get the lock on the window + rv = GetLock(w, &destroyed); + + if (NS_SUCCEEDED(rv)) { + // send our command + rv = DoSendCommandLine(w, argc, argv, aStartupToken, aResponse, + &destroyed); + + // if the window was destroyed, don't bother trying to free the + // lock. + if (!destroyed) FreeLock(w); // doesn't really matter what this returns + } + } + + XSetErrorHandler(sOldHandler); + + MOZ_LOG(sRemoteLm, LogLevel::Debug, + ("SendCommandInternal returning 0x%" PRIx32 "\n", + static_cast<uint32_t>(rv))); + + return rv; +} + +Window nsXRemoteClient::CheckWindow(Window aWindow) { + Atom type = None; + int format; + unsigned long nitems, bytesafter; + unsigned char* data; + Window innerWindow; + + XGetWindowProperty(mDisplay, aWindow, mMozWMStateAtom, 0, 0, False, + AnyPropertyType, &type, &format, &nitems, &bytesafter, + &data); + + if (type) { + XFree(data); + return aWindow; + } + + // didn't find it here so check the children of this window + innerWindow = CheckChildren(aWindow); + + if (innerWindow) return innerWindow; + + return aWindow; +} + +Window nsXRemoteClient::CheckChildren(Window aWindow) { + Window root, parent; + Window* children; + unsigned int nchildren; + unsigned int i; + Atom type = None; + int format; + unsigned long nitems, after; + unsigned char* data; + Window retval = None; + + if (!XQueryTree(mDisplay, aWindow, &root, &parent, &children, &nchildren)) + return None; + + // scan the list first before recursing into the list of windows + // which can get quite deep. + for (i = 0; !retval && (i < nchildren); i++) { + XGetWindowProperty(mDisplay, children[i], mMozWMStateAtom, 0, 0, False, + AnyPropertyType, &type, &format, &nitems, &after, &data); + if (type) { + XFree(data); + retval = children[i]; + } + } + + // otherwise recurse into the list + for (i = 0; !retval && (i < nchildren); i++) { + retval = CheckChildren(children[i]); + } + + if (children) XFree((char*)children); + + return retval; +} + +nsresult nsXRemoteClient::GetLock(Window aWindow, bool* aDestroyed) { + bool locked = false; + bool waited = false; + *aDestroyed = false; + + nsresult rv = NS_OK; + + if (!mLockData) { + char pidstr[32]; + char sysinfobuf[SYS_INFO_BUFFER_LENGTH]; + SprintfLiteral(pidstr, "pid%d@", getpid()); + PRStatus status; + status = + PR_GetSystemInfo(PR_SI_HOSTNAME, sysinfobuf, SYS_INFO_BUFFER_LENGTH); + if (status != PR_SUCCESS) { + return NS_ERROR_FAILURE; + } + + // allocate enough space for the string plus the terminating + // char + mLockData = (char*)malloc(strlen(pidstr) + strlen(sysinfobuf) + 1); + if (!mLockData) return NS_ERROR_OUT_OF_MEMORY; + + strcpy(mLockData, pidstr); + if (!strcat(mLockData, sysinfobuf)) return NS_ERROR_FAILURE; + } + + do { + int result; + Atom actual_type; + int actual_format; + unsigned long nitems, bytes_after; + unsigned char* data = 0; + + XGrabServer(mDisplay); + + result = XGetWindowProperty( + mDisplay, aWindow, mMozLockAtom, 0, (65536 / sizeof(long)), + False, /* don't delete */ + XA_STRING, &actual_type, &actual_format, &nitems, &bytes_after, &data); + + // aWindow may have been destroyed before XSelectInput was processed, in + // which case there may not be any DestroyNotify event in the queue to + // tell us. XGetWindowProperty() was synchronous so error responses have + // now been processed, setting sGotBadWindow. + if (sGotBadWindow) { + *aDestroyed = true; + rv = NS_ERROR_FAILURE; + } else if (result != Success || actual_type == None) { + /* It's not now locked - lock it. */ + XChangeProperty(mDisplay, aWindow, mMozLockAtom, XA_STRING, 8, + PropModeReplace, (unsigned char*)mLockData, + strlen(mLockData)); + locked = True; + } + + XUngrabServer(mDisplay); + XFlush(mDisplay); // ungrab now! + + if (!locked && !NS_FAILED(rv)) { + /* We tried to grab the lock this time, and failed because someone + else is holding it already. So, wait for a PropertyDelete event + to come in, and try again. */ + MOZ_LOG(sRemoteLm, LogLevel::Debug, + ("window 0x%x is locked by %s; waiting...\n", + (unsigned int)aWindow, data)); + waited = True; + while (true) { + XEvent event; + int poll_retval; + struct pollfd pfd; + + pfd.fd = ConnectionNumber(mDisplay); + pfd.events = POLLIN; + + poll_retval = poll(&pfd, 1, 10 * 1000); + // did we time out? + if (poll_retval == 0) { + MOZ_LOG(sRemoteLm, LogLevel::Debug, + ("timed out waiting for window\n")); + rv = NS_ERROR_FAILURE; + break; + } + MOZ_LOG(sRemoteLm, LogLevel::Debug, ("xevent...\n")); + // FIXME check the return value from this? + XNextEvent(mDisplay, &event); + if (event.xany.type == DestroyNotify && + event.xdestroywindow.window == aWindow) { + *aDestroyed = true; + rv = NS_ERROR_FAILURE; + break; + } + if (event.xany.type == PropertyNotify && + event.xproperty.state == PropertyDelete && + event.xproperty.window == aWindow && + event.xproperty.atom == mMozLockAtom) { + /* Ok! Someone deleted their lock, so now we can try + again. */ + MOZ_LOG( + sRemoteLm, LogLevel::Debug, + ("(0x%x unlocked, trying again...)\n", (unsigned int)aWindow)); + break; + } + } + } + if (data) XFree(data); + } while (!locked && !NS_FAILED(rv)); + + if (waited && locked) { + MOZ_LOG(sRemoteLm, LogLevel::Debug, ("obtained lock.\n")); + } else if (*aDestroyed) { + MOZ_LOG(sRemoteLm, LogLevel::Debug, + ("window 0x%x unexpectedly destroyed.\n", (unsigned int)aWindow)); + } + + return rv; +} + +Window nsXRemoteClient::FindBestWindow(const char* aProgram, + const char* aProfile) { + Window root = RootWindowOfScreen(DefaultScreenOfDisplay(mDisplay)); + Window bestWindow = 0; + Window root2, parent, *kids; + unsigned int nkids; + + // Get a list of the children of the root window, walk the list + // looking for the best window that fits the criteria. + if (!XQueryTree(mDisplay, root, &root2, &parent, &kids, &nkids)) { + MOZ_LOG(sRemoteLm, LogLevel::Debug, + ("XQueryTree failed in nsXRemoteClient::FindBestWindow")); + return 0; + } + + if (!(kids && nkids)) { + MOZ_LOG(sRemoteLm, LogLevel::Debug, ("root window has no children")); + return 0; + } + + // We'll walk the list of windows looking for a window that best + // fits the criteria here. + + for (unsigned int i = 0; i < nkids; i++) { + Atom type; + int format; + unsigned long nitems, bytesafter; + unsigned char* data_return = 0; + Window w; + w = kids[i]; + // find the inner window with WM_STATE on it + w = CheckWindow(w); + + int status = XGetWindowProperty( + mDisplay, w, mMozVersionAtom, 0, (65536 / sizeof(long)), False, + XA_STRING, &type, &format, &nitems, &bytesafter, &data_return); + + if (!data_return) continue; + + double version = PR_strtod((char*)data_return, nullptr); + XFree(data_return); + + if (!(version >= 5.1 && version < 6)) continue; + + data_return = 0; + + if (status != Success || type == None) continue; + + // Check that this window is from the right program. + Unused << XGetWindowProperty( + mDisplay, w, mMozProgramAtom, 0, (65536 / sizeof(long)), False, + XA_STRING, &type, &format, &nitems, &bytesafter, &data_return); + + // If the return name is not the same as this program name, we don't want + // this window. + if (data_return) { + if (strcmp(aProgram, (const char*)data_return)) { + XFree(data_return); + continue; + } + + // This is actually the success condition. + XFree(data_return); + } else { + // Doesn't support the protocol, even though the user + // requested it. So we're not going to use this window. + continue; + } + + // Check to see if it has the user atom on that window. If there + // is then we need to make sure that it matches what we have. + const char* username = PR_GetEnv("LOGNAME"); + + if (username) { + Unused << XGetWindowProperty( + mDisplay, w, mMozUserAtom, 0, (65536 / sizeof(long)), False, + XA_STRING, &type, &format, &nitems, &bytesafter, &data_return); + + // if there's a username compare it with what we have + if (data_return) { + // If the IDs aren't equal, we don't want this window. + if (strcmp(username, (const char*)data_return)) { + XFree(data_return); + continue; + } + + XFree(data_return); + } + } + + // Check to see if there's a profile name on this window. If + // there is, then we need to make sure it matches what someone + // passed in. + Unused << XGetWindowProperty( + mDisplay, w, mMozProfileAtom, 0, (65536 / sizeof(long)), False, + XA_STRING, &type, &format, &nitems, &bytesafter, &data_return); + + // If there's a profile compare it with what we have + if (data_return) { + // If the profiles aren't equal, we don't want this window. + if (strcmp(aProfile, (const char*)data_return)) { + XFree(data_return); + continue; + } + + XFree(data_return); + } else { + // This isn't the window for this profile. + continue; + } + + // Check to see if the window supports the new command-line passing + // protocol, if that is requested. + + // If we got this far, this is the best window. It passed + // all the tests. + bestWindow = w; + break; + } + + if (kids) XFree((char*)kids); + + return bestWindow; +} + +nsresult nsXRemoteClient::FreeLock(Window aWindow) { + int result; + Atom actual_type; + int actual_format; + unsigned long nitems, bytes_after; + unsigned char* data = 0; + + result = XGetWindowProperty( + mDisplay, aWindow, mMozLockAtom, 0, (65536 / sizeof(long)), + True, /* atomic delete after */ + XA_STRING, &actual_type, &actual_format, &nitems, &bytes_after, &data); + if (result != Success) { + MOZ_LOG(sRemoteLm, LogLevel::Debug, + ("unable to read and delete " MOZILLA_LOCK_PROP " property\n")); + return NS_ERROR_FAILURE; + } + if (!data || !*data) { + MOZ_LOG(sRemoteLm, LogLevel::Debug, + ("invalid data on " MOZILLA_LOCK_PROP " of window 0x%x.\n", + (unsigned int)aWindow)); + return NS_ERROR_FAILURE; + } else if (strcmp((char*)data, mLockData)) { + MOZ_LOG(sRemoteLm, LogLevel::Debug, + (MOZILLA_LOCK_PROP " was stolen! Expected \"%s\", saw \"%s\"!\n", + mLockData, data)); + return NS_ERROR_FAILURE; + } + + if (data) XFree(data); + return NS_OK; +} + +nsresult nsXRemoteClient::DoSendCommandLine(Window aWindow, int32_t argc, + char** argv, + const char* aStartupToken, + char** aResponse, + bool* aDestroyed) { + *aDestroyed = false; + + int commandLineLength; + char* commandLine = + ConstructCommandLine(argc, argv, aStartupToken, &commandLineLength); + XChangeProperty(mDisplay, aWindow, mMozCommandLineAtom, XA_STRING, 8, + PropModeReplace, (unsigned char*)commandLine, + commandLineLength); + free(commandLine); + + if (!WaitForResponse(aWindow, aResponse, aDestroyed, mMozCommandLineAtom)) + return NS_ERROR_FAILURE; + + return NS_OK; +} + +bool nsXRemoteClient::WaitForResponse(Window aWindow, char** aResponse, + bool* aDestroyed, Atom aCommandAtom) { + bool done = false; + bool accepted = false; + + while (!done) { + XEvent event; + XNextEvent(mDisplay, &event); + if (event.xany.type == DestroyNotify && + event.xdestroywindow.window == aWindow) { + /* Print to warn user...*/ + MOZ_LOG(sRemoteLm, LogLevel::Debug, + ("window 0x%x was destroyed.\n", (unsigned int)aWindow)); + *aResponse = strdup("Window was destroyed while reading response."); + *aDestroyed = true; + return false; + } + if (event.xany.type == PropertyNotify && + event.xproperty.state == PropertyNewValue && + event.xproperty.window == aWindow && + event.xproperty.atom == mMozResponseAtom) { + Atom actual_type; + int actual_format; + unsigned long nitems, bytes_after; + unsigned char* data = 0; + Bool result; + result = XGetWindowProperty(mDisplay, aWindow, mMozResponseAtom, 0, + (65536 / sizeof(long)), + True, /* atomic delete after */ + XA_STRING, &actual_type, &actual_format, + &nitems, &bytes_after, &data); + if (result != Success) { + MOZ_LOG( + sRemoteLm, LogLevel::Debug, + ("failed reading " MOZILLA_RESPONSE_PROP " from window 0x%0x.\n", + (unsigned int)aWindow)); + *aResponse = strdup("Internal error reading response from window."); + done = true; + } else if (!data || strlen((char*)data) < 5) { + MOZ_LOG(sRemoteLm, LogLevel::Debug, + ("invalid data on " MOZILLA_RESPONSE_PROP + " property of window 0x%0x.\n", + (unsigned int)aWindow)); + *aResponse = strdup("Server returned invalid data in response."); + done = true; + } else if (*data == '1') { /* positive preliminary reply */ + MOZ_LOG(sRemoteLm, LogLevel::Debug, ("%s\n", data + 4)); + /* keep going */ + done = false; + } + + else if (!strncmp((char*)data, "200", 3)) { /* positive completion */ + *aResponse = strdup((char*)data); + accepted = true; + done = true; + } + + else if (*data == '2') { /* positive completion */ + MOZ_LOG(sRemoteLm, LogLevel::Debug, ("%s\n", data + 4)); + *aResponse = strdup((char*)data); + accepted = true; + done = true; + } + + else if (*data == '3') { /* positive intermediate reply */ + MOZ_LOG(sRemoteLm, LogLevel::Debug, + ("internal error: " + "server wants more information? (%s)\n", + data)); + *aResponse = strdup((char*)data); + done = true; + } + + else if (*data == '4' || /* transient negative completion */ + *data == '5') { /* permanent negative completion */ + MOZ_LOG(sRemoteLm, LogLevel::Debug, ("%s\n", data + 4)); + *aResponse = strdup((char*)data); + done = true; + } + + else { + MOZ_LOG( + sRemoteLm, LogLevel::Debug, + ("unrecognised " MOZILLA_RESPONSE_PROP " from window 0x%x: %s\n", + (unsigned int)aWindow, data)); + *aResponse = strdup((char*)data); + done = true; + } + + if (data) XFree(data); + } + + else if (event.xany.type == PropertyNotify && + event.xproperty.window == aWindow && + event.xproperty.state == PropertyDelete && + event.xproperty.atom == aCommandAtom) { + MOZ_LOG(sRemoteLm, LogLevel::Debug, + ("(server 0x%x has accepted " MOZILLA_COMMANDLINE_PROP ".)\n", + (unsigned int)aWindow)); + } + } + + return accepted; +} diff --git a/toolkit/components/remote/nsXRemoteClient.h b/toolkit/components/remote/nsXRemoteClient.h new file mode 100644 index 0000000000..ad8e7d3787 --- /dev/null +++ b/toolkit/components/remote/nsXRemoteClient.h @@ -0,0 +1,49 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <X11/X.h> +#include <X11/Xlib.h> + +#include "nsRemoteClient.h" + +class nsXRemoteClient : public nsRemoteClient { + public: + nsXRemoteClient(); + ~nsXRemoteClient(); + + virtual nsresult Init() override; + virtual nsresult SendCommandLine(const char* aProgram, const char* aProfile, + int32_t argc, char** argv, + const char* aStartupToken, char** aResponse, + bool* aSucceeded) override; + void Shutdown(); + + private: + Window CheckWindow(Window aWindow); + Window CheckChildren(Window aWindow); + nsresult GetLock(Window aWindow, bool* aDestroyed); + nsresult FreeLock(Window aWindow); + Window FindBestWindow(const char* aProgram, const char* aProfile); + nsresult DoSendCommandLine(Window aWindow, int32_t argc, char** argv, + const char* aStartupToken, char** aResponse, + bool* aDestroyed); + bool WaitForResponse(Window aWindow, char** aResponse, bool* aDestroyed, + Atom aCommandAtom); + + Display* mDisplay; + + Atom mMozVersionAtom; + Atom mMozLockAtom; + Atom mMozCommandLineAtom; + Atom mMozResponseAtom; + Atom mMozWMStateAtom; + Atom mMozUserAtom; + Atom mMozProfileAtom; + Atom mMozProgramAtom; + + char* mLockData; + + bool mInitialized; +}; diff --git a/toolkit/components/remote/nsXRemoteServer.cpp b/toolkit/components/remote/nsXRemoteServer.cpp new file mode 100644 index 0000000000..1f97b18e20 --- /dev/null +++ b/toolkit/components/remote/nsXRemoteServer.cpp @@ -0,0 +1,161 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:expandtab:shiftwidth=2:tabstop=8: + */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/ArrayUtils.h" + +#include "nsXRemoteServer.h" +#include "nsCOMPtr.h" +#include "nsICommandLine.h" + +#include "nsIWidget.h" +#include "nsAppShellCID.h" +#include "nsPIDOMWindow.h" +#include "mozilla/X11Util.h" + +#include "nsCOMPtr.h" +#include "nsString.h" +#include "prenv.h" +#include "nsCRT.h" + +#include "nsXULAppAPI.h" + +#include <X11/Xlib.h> +#include <X11/Xatom.h> + +using namespace mozilla; + +#define MOZILLA_VERSION_PROP "_MOZILLA_VERSION" +#define MOZILLA_LOCK_PROP "_MOZILLA_LOCK" +#define MOZILLA_RESPONSE_PROP "_MOZILLA_RESPONSE" +#define MOZILLA_USER_PROP "_MOZILLA_USER" +#define MOZILLA_PROFILE_PROP "_MOZILLA_PROFILE" +#define MOZILLA_PROGRAM_PROP "_MOZILLA_PROGRAM" +#define MOZILLA_COMMANDLINE_PROP "_MOZILLA_COMMANDLINE" + +const unsigned char kRemoteVersion[] = "5.1"; + +// Minimize the roundtrips to the X server by getting all the atoms at once +static const char* XAtomNames[] = { + MOZILLA_VERSION_PROP, MOZILLA_LOCK_PROP, MOZILLA_RESPONSE_PROP, + MOZILLA_USER_PROP, MOZILLA_PROFILE_PROP, MOZILLA_PROGRAM_PROP, + MOZILLA_COMMANDLINE_PROP}; +static Atom XAtoms[MOZ_ARRAY_LENGTH(XAtomNames)]; + +Atom nsXRemoteServer::sMozVersionAtom; +Atom nsXRemoteServer::sMozLockAtom; +Atom nsXRemoteServer::sMozResponseAtom; +Atom nsXRemoteServer::sMozUserAtom; +Atom nsXRemoteServer::sMozProfileAtom; +Atom nsXRemoteServer::sMozProgramAtom; +Atom nsXRemoteServer::sMozCommandLineAtom; + +nsXRemoteServer::nsXRemoteServer() = default; + +void nsXRemoteServer::XRemoteBaseStartup(const char* aAppName, + const char* aProfileName) { + EnsureAtoms(); + + mAppName = aAppName; + ToLowerCase(mAppName); + + mProfileName = aProfileName; +} + +void nsXRemoteServer::HandleCommandsFor(Window aWindowId) { + // set our version + XChangeProperty(mozilla::DefaultXDisplay(), aWindowId, sMozVersionAtom, + XA_STRING, 8, PropModeReplace, kRemoteVersion, + sizeof(kRemoteVersion) - 1); + + // get our username + unsigned char* logname; + logname = (unsigned char*)PR_GetEnv("LOGNAME"); + if (logname) { + // set the property on the window if it's available + XChangeProperty(mozilla::DefaultXDisplay(), aWindowId, sMozUserAtom, + XA_STRING, 8, PropModeReplace, logname, + strlen((char*)logname)); + } + + XChangeProperty(mozilla::DefaultXDisplay(), aWindowId, sMozProgramAtom, + XA_STRING, 8, PropModeReplace, (unsigned char*)mAppName.get(), + mAppName.Length()); + + XChangeProperty(mozilla::DefaultXDisplay(), aWindowId, sMozProfileAtom, + XA_STRING, 8, PropModeReplace, + (unsigned char*)mProfileName.get(), mProfileName.Length()); +} + +bool nsXRemoteServer::HandleNewProperty(XID aWindowId, Display* aDisplay, + Time aEventTime, Atom aChangedAtom) { + if (aChangedAtom == sMozCommandLineAtom) { + // We got a new command atom. + int result; + Atom actual_type; + int actual_format; + unsigned long nitems, bytes_after; + char* data = 0; + + result = XGetWindowProperty(aDisplay, aWindowId, aChangedAtom, + 0, /* long_offset */ + (65536 / sizeof(long)), /* long_length */ + X11True, /* atomic delete after */ + XA_STRING, /* req_type */ + &actual_type, /* actual_type return */ + &actual_format, /* actual_format_return */ + &nitems, /* nitems_return */ + &bytes_after, /* bytes_after_return */ + (unsigned char**)&data); /* prop_return + (we only care + about the first ) */ + + // Failed to get property off the window? + if (result != Success) return false; + + // Failed to get the data off the window or it was the wrong type? + if (!data || !TO_LITTLE_ENDIAN32(*reinterpret_cast<int32_t*>(data))) + return false; + + // cool, we got the property data. + const char* response = HandleCommandLine(data, aEventTime); + + // put the property onto the window as the response + XChangeProperty(aDisplay, aWindowId, sMozResponseAtom, XA_STRING, 8, + PropModeReplace, (const unsigned char*)response, + strlen(response)); + XFree(data); + return true; + } + + if (aChangedAtom == sMozResponseAtom) { + // client accepted the response. party on wayne. + return true; + } + + else if (aChangedAtom == sMozLockAtom) { + // someone locked the window + return true; + } + + return false; +} + +void nsXRemoteServer::EnsureAtoms(void) { + if (sMozVersionAtom) return; + + XInternAtoms(mozilla::DefaultXDisplay(), const_cast<char**>(XAtomNames), + ArrayLength(XAtomNames), X11False, XAtoms); + + int i = 0; + sMozVersionAtom = XAtoms[i++]; + sMozLockAtom = XAtoms[i++]; + sMozResponseAtom = XAtoms[i++]; + sMozUserAtom = XAtoms[i++]; + sMozProfileAtom = XAtoms[i++]; + sMozProgramAtom = XAtoms[i++]; + sMozCommandLineAtom = XAtoms[i]; +} diff --git a/toolkit/components/remote/nsXRemoteServer.h b/toolkit/components/remote/nsXRemoteServer.h new file mode 100644 index 0000000000..405bafe789 --- /dev/null +++ b/toolkit/components/remote/nsXRemoteServer.h @@ -0,0 +1,44 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:expandtab:shiftwidth=2:tabstop=8: + */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef NSXREMOTESERVER_H +#define NSXREMOTESERVER_H + +#include "nsString.h" +#include "nsRemoteServer.h" +#include "nsUnixRemoteServer.h" + +#include <X11/Xlib.h> +#include <X11/X.h> + +/** + Base class for GTK/Qt remote service +*/ +class nsXRemoteServer : public nsRemoteServer, public nsUnixRemoteServer { + protected: + nsXRemoteServer(); + bool HandleNewProperty(Window aWindowId, Display* aDisplay, Time aEventTime, + Atom aChangedAtom); + void XRemoteBaseStartup(const char* aAppName, const char* aProfileName); + void HandleCommandsFor(Window aWindowId); + + private: + void EnsureAtoms(); + + nsCString mAppName; + nsCString mProfileName; + + static Atom sMozVersionAtom; + static Atom sMozLockAtom; + static Atom sMozResponseAtom; + static Atom sMozUserAtom; + static Atom sMozProfileAtom; + static Atom sMozProgramAtom; + static Atom sMozCommandLineAtom; +}; + +#endif // NSXREMOTESERVER_H diff --git a/toolkit/components/remotebrowserutils/RemoteWebNavigation.sys.mjs b/toolkit/components/remotebrowserutils/RemoteWebNavigation.sys.mjs new file mode 100644 index 0000000000..2d33706427 --- /dev/null +++ b/toolkit/components/remotebrowserutils/RemoteWebNavigation.sys.mjs @@ -0,0 +1,209 @@ +// -*- indent-tabs-mode: nil; js-indent-level: 2 -*- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +const lazy = {}; +ChromeUtils.defineESModuleGetters(lazy, { + PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", +}); + +// This object implements the JS parts of nsIWebNavigation. +export class RemoteWebNavigation { + constructor(aBrowser) { + this._browser = aBrowser; + this._cancelContentJSEpoch = 1; + this._currentURI = null; + this._canGoBack = false; + this._canGoForward = false; + this.referringURI = null; + } + + swapBrowser(aBrowser) { + this._browser = aBrowser; + } + + maybeCancelContentJSExecution(aNavigationType, aOptions = {}) { + const epoch = this._cancelContentJSEpoch++; + this._browser.frameLoader.remoteTab.maybeCancelContentJSExecution( + aNavigationType, + { ...aOptions, epoch } + ); + return epoch; + } + + get canGoBack() { + if (Services.appinfo.sessionHistoryInParent) { + return this._browser.browsingContext.sessionHistory?.index > 0; + } + return this._canGoBack; + } + + get canGoForward() { + if (Services.appinfo.sessionHistoryInParent) { + let sessionHistory = this._browser.browsingContext.sessionHistory; + return sessionHistory?.index < sessionHistory?.count - 1; + } + return this._canGoForward; + } + + goBack(requireUserInteraction = false) { + let cancelContentJSEpoch = this.maybeCancelContentJSExecution( + Ci.nsIRemoteTab.NAVIGATE_BACK + ); + this._browser.browsingContext.goBack( + cancelContentJSEpoch, + requireUserInteraction, + true + ); + } + goForward(requireUserInteraction = false) { + let cancelContentJSEpoch = this.maybeCancelContentJSExecution( + Ci.nsIRemoteTab.NAVIGATE_FORWARD + ); + this._browser.browsingContext.goForward( + cancelContentJSEpoch, + requireUserInteraction, + true + ); + } + gotoIndex(aIndex) { + let cancelContentJSEpoch = this.maybeCancelContentJSExecution( + Ci.nsIRemoteTab.NAVIGATE_INDEX, + { index: aIndex } + ); + this._browser.browsingContext.goToIndex(aIndex, cancelContentJSEpoch, true); + } + + _speculativeConnect(uri, loadURIOptions) { + try { + // Let's start a network connection before the content process asks. + // Note that we might have already set up the speculative connection in + // some cases, especially when the url is from location bar or its popup + // menu. + if (uri.schemeIs("http") || uri.schemeIs("https")) { + let isBrowserPrivate = lazy.PrivateBrowsingUtils.isBrowserPrivate( + this._browser + ); + let principal = loadURIOptions.triggeringPrincipal; + // We usually have a triggeringPrincipal assigned, but in case we + // don't have one or if it's a SystemPrincipal, let's create it with OA + // inferred from the current context. + if (!principal || principal.isSystemPrincipal) { + let attrs = { + userContextId: this._browser.getAttribute("usercontextid") || 0, + privateBrowsingId: isBrowserPrivate ? 1 : 0, + }; + principal = Services.scriptSecurityManager.createContentPrincipal( + uri, + attrs + ); + } + Services.io.speculativeConnect(uri, principal, null, false); + } + } catch (ex) { + // Can't setup speculative connection for this uri for some + // reason, just ignore it. + } + } + + loadURI(uri, loadURIOptions) { + this._speculativeConnect(uri, loadURIOptions); + let cancelContentJSEpoch = this.maybeCancelContentJSExecution( + Ci.nsIRemoteTab.NAVIGATE_URL, + { uri } + ); + this._browser.browsingContext.loadURI(uri, { + ...loadURIOptions, + cancelContentJSEpoch, + }); + } + + fixupAndLoadURIString(uriString, loadURIOptions) { + let uri; + try { + let fixupFlags = Services.uriFixup.webNavigationFlagsToFixupFlags( + uriString, + loadURIOptions.loadFlags + ); + let isBrowserPrivate = lazy.PrivateBrowsingUtils.isBrowserPrivate( + this._browser + ); + if (isBrowserPrivate) { + fixupFlags |= Services.uriFixup.FIXUP_FLAG_PRIVATE_CONTEXT; + } + + uri = Services.uriFixup.getFixupURIInfo( + uriString, + fixupFlags + ).preferredURI; + } catch (ex) { + // In rare cases `uriFixup` can throw. We ignore this here, but it's + // likely that the fixupAndLoadURIString call below will still throw, + // hopefully with more details. + } + if (uri) { + this._speculativeConnect(uri, loadURIOptions); + } + + let cancelContentJSEpoch = this.maybeCancelContentJSExecution( + Ci.nsIRemoteTab.NAVIGATE_URL, + { uri } + ); + // For now, continue to use fixup here, but note that ideally we should be + // doing fixup only once and reusing the URI we created above. Addressing + // this is bug 1815509. + this._browser.browsingContext.fixupAndLoadURIString(uriString, { + ...loadURIOptions, + cancelContentJSEpoch, + }); + } + + reload(aReloadFlags) { + this._browser.browsingContext.reload(aReloadFlags); + } + stop(aStopFlags) { + this._browser.browsingContext.stop(aStopFlags); + } + + get document() { + return this._browser.contentDocument; + } + + get currentURI() { + if (!this._currentURI) { + this._currentURI = Services.io.newURI("about:blank"); + } + return this._currentURI; + } + set currentURI(aURI) { + // Bug 1498600 verify usages of systemPrincipal here + let loadURIOptions = { + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + }; + this.loadURI(aURI.spec, loadURIOptions); + } + + // Bug 1233803 - accessing the sessionHistory of remote browsers should be + // done in content scripts. + get sessionHistory() { + throw new Components.Exception( + "Not implemented", + Cr.NS_ERROR_NOT_IMPLEMENTED + ); + } + set sessionHistory(aValue) { + throw new Components.Exception( + "Not implemented", + Cr.NS_ERROR_NOT_IMPLEMENTED + ); + } + + _sendMessage(aMessage, aData) { + try { + this._browser.sendMessageToActor(aMessage, aData, "WebNavigation"); + } catch (e) { + console.error(e); + } + } +} diff --git a/toolkit/components/remotebrowserutils/moz.build b/toolkit/components/remotebrowserutils/moz.build new file mode 100644 index 0000000000..0723c716ff --- /dev/null +++ b/toolkit/components/remotebrowserutils/moz.build @@ -0,0 +1,14 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +with Files("**"): + BUG_COMPONENT = ("Core", "DOM: Navigation") + +EXTRA_JS_MODULES += [ + "RemoteWebNavigation.sys.mjs", +] + +BROWSER_CHROME_MANIFESTS += ["tests/browser/browser.ini"] diff --git a/toolkit/components/remotebrowserutils/tests/browser/307redirect.sjs b/toolkit/components/remotebrowserutils/tests/browser/307redirect.sjs new file mode 100644 index 0000000000..1a28b8c5c3 --- /dev/null +++ b/toolkit/components/remotebrowserutils/tests/browser/307redirect.sjs @@ -0,0 +1,6 @@ +function handleRequest(request, response) { + response.setStatusLine(request.httpVersion, 307, "Temporary Redirect"); + let location = request.queryString; + response.setHeader("Location", location, false); + response.write("Hello world!"); +} diff --git a/toolkit/components/remotebrowserutils/tests/browser/browser.ini b/toolkit/components/remotebrowserutils/tests/browser/browser.ini new file mode 100644 index 0000000000..a7d0a4786d --- /dev/null +++ b/toolkit/components/remotebrowserutils/tests/browser/browser.ini @@ -0,0 +1,16 @@ +[DEFAULT] +support-files = + dummy_page.html + print_postdata.sjs + 307redirect.sjs + head.js + coop_header.sjs + file_postmsg_parent.html + +[browser_RemoteWebNavigation.js] +https_first_disabled = true +[browser_documentChannel.js] +[browser_httpCrossOriginOpenerPolicy.js] +[browser_httpToFileHistory.js] +[browser_oopProcessSwap.js] +[browser_externalLinkBlanksPage.js] diff --git a/toolkit/components/remotebrowserutils/tests/browser/browser_RemoteWebNavigation.js b/toolkit/components/remotebrowserutils/tests/browser/browser_RemoteWebNavigation.js new file mode 100644 index 0000000000..22952e85f5 --- /dev/null +++ b/toolkit/components/remotebrowserutils/tests/browser/browser_RemoteWebNavigation.js @@ -0,0 +1,238 @@ +const SYSTEMPRINCIPAL = Services.scriptSecurityManager.getSystemPrincipal(); +const DUMMY1 = + "http://test1.example.org/browser/toolkit/modules/tests/browser/dummy_page.html"; +const DUMMY2 = + "http://test2.example.org/browser/toolkit/modules/tests/browser/dummy_page.html"; +const LOAD_URI_OPTIONS = { triggeringPrincipal: SYSTEMPRINCIPAL }; + +function waitForLoad(uri) { + return BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser, false, uri); +} + +function waitForPageShow(browser = gBrowser.selectedBrowser) { + return BrowserTestUtils.waitForContentEvent(browser, "pageshow", true); +} + +// Tests that loadURI accepts a referrer and it is included in the load. +add_task(async function test_referrer() { + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + let browser = gBrowser.selectedBrowser; + let ReferrerInfo = Components.Constructor( + "@mozilla.org/referrer-info;1", + "nsIReferrerInfo", + "init" + ); + + let loadURIOptionsWithReferrer = { + triggeringPrincipal: SYSTEMPRINCIPAL, + referrerInfo: new ReferrerInfo( + Ci.nsIReferrerInfo.EMPTY, + true, + Services.io.newURI(DUMMY2) + ), + }; + browser.webNavigation.loadURI( + Services.io.newURI(DUMMY1), + loadURIOptionsWithReferrer + ); + await waitForLoad(DUMMY1); + + await SpecialPowers.spawn( + browser, + [[DUMMY1, DUMMY2]], + function ([dummy1, dummy2]) { + function getExpectedReferrer(referrer) { + let defaultPolicy = Services.prefs.getIntPref( + "network.http.referer.defaultPolicy" + ); + ok( + [2, 3].indexOf(defaultPolicy) > -1, + "default referrer policy should be either strict-origin-when-cross-origin(2) or no-referrer-when-downgrade(3)" + ); + if (defaultPolicy == 2) { + return referrer.match(/https?:\/\/[^\/]+\/?/i)[0]; + } + return referrer; + } + + is(content.location.href, dummy1, "Should have loaded the right URL"); + is( + content.document.referrer, + getExpectedReferrer(dummy2), + "Should have the right referrer" + ); + } + ); + + gBrowser.removeCurrentTab(); +}); + +// Tests that remote access to webnavigation.sessionHistory works. +add_task(async function test_history() { + async function checkHistoryIndex(browser, n) { + if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) { + return SpecialPowers.spawn(browser, [n], function (n) { + let history = + docShell.browsingContext.childSessionHistory.legacySHistory; + + is(history.index, n, "Should be at the right place in history"); + }); + } + + let history = browser.browsingContext.sessionHistory; + is(history.index, n, "Should be at the right place in history"); + return null; + } + + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + let browser = gBrowser.selectedBrowser; + + browser.webNavigation.loadURI(Services.io.newURI(DUMMY1), LOAD_URI_OPTIONS); + await waitForLoad(DUMMY1); + + browser.webNavigation.loadURI(Services.io.newURI(DUMMY2), LOAD_URI_OPTIONS); + await waitForLoad(DUMMY2); + + if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) { + await SpecialPowers.spawn( + browser, + [[DUMMY1, DUMMY2]], + function ([dummy1, dummy2]) { + let history = + docShell.browsingContext.childSessionHistory.legacySHistory; + + is(history.count, 2, "Should be two history items"); + is(history.index, 1, "Should be at the right place in history"); + let entry = history.getEntryAtIndex(0); + is(entry.URI.spec, dummy1, "Should have the right history entry"); + entry = history.getEntryAtIndex(1); + is(entry.URI.spec, dummy2, "Should have the right history entry"); + } + ); + } else { + let history = browser.browsingContext.sessionHistory; + + is(history.count, 2, "Should be two history items"); + is(history.index, 1, "Should be at the right place in history"); + let entry = history.getEntryAtIndex(0); + is(entry.URI.spec, DUMMY1, "Should have the right history entry"); + entry = history.getEntryAtIndex(1); + is(entry.URI.spec, DUMMY2, "Should have the right history entry"); + } + + let promise = waitForPageShow(); + browser.webNavigation.goBack(); + await promise; + await checkHistoryIndex(browser, 0); + + promise = waitForPageShow(); + browser.webNavigation.goForward(); + await promise; + await checkHistoryIndex(browser, 1); + + promise = waitForPageShow(); + browser.webNavigation.gotoIndex(0); + await promise; + await checkHistoryIndex(browser, 0); + + gBrowser.removeCurrentTab(); +}); + +// Tests that load flags are passed through to the content process. +add_task(async function test_flags() { + async function checkHistory(browser, { count, index }) { + if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) { + return SpecialPowers.spawn( + browser, + [[DUMMY2, count, index]], + function ([dummy2, count, index]) { + let history = + docShell.browsingContext.childSessionHistory.legacySHistory; + is(history.count, count, "Should be one history item"); + is(history.index, index, "Should be at the right place in history"); + let entry = history.getEntryAtIndex(index); + is(entry.URI.spec, dummy2, "Should have the right history entry"); + } + ); + } + + let history = browser.browsingContext.sessionHistory; + is(history.count, count, "Should be one history item"); + is(history.index, index, "Should be at the right place in history"); + let entry = history.getEntryAtIndex(index); + is(entry.URI.spec, DUMMY2, "Should have the right history entry"); + + return null; + } + + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + let browser = gBrowser.selectedBrowser; + + browser.webNavigation.loadURI(Services.io.newURI(DUMMY1), LOAD_URI_OPTIONS); + await waitForLoad(DUMMY1); + let loadURIOptionsReplaceHistory = { + triggeringPrincipal: SYSTEMPRINCIPAL, + loadFlags: Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY, + }; + browser.webNavigation.loadURI( + Services.io.newURI(DUMMY2), + loadURIOptionsReplaceHistory + ); + await waitForLoad(DUMMY2); + await checkHistory(browser, { count: 1, index: 0 }); + let loadURIOptionsBypassHistory = { + triggeringPrincipal: SYSTEMPRINCIPAL, + loadFlags: Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_HISTORY, + }; + browser.webNavigation.loadURI( + Services.io.newURI(DUMMY1), + loadURIOptionsBypassHistory + ); + await waitForLoad(DUMMY1); + await checkHistory(browser, { count: 1, index: 0 }); + + gBrowser.removeCurrentTab(); +}); + +// Tests that attempts to use unsupported arguments throw an exception. +add_task(async function test_badarguments() { + if (!gMultiProcessBrowser) { + return; + } + + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + let browser = gBrowser.selectedBrowser; + + try { + let loadURIOptionsBadPostData = { + triggeringPrincipal: SYSTEMPRINCIPAL, + postData: {}, + }; + browser.webNavigation.loadURI( + Services.io.newURI(DUMMY1), + loadURIOptionsBadPostData + ); + ok( + false, + "Should have seen an exception from trying to pass some postdata" + ); + } catch (e) { + ok(true, "Should have seen an exception from trying to pass some postdata"); + } + + try { + let loadURIOptionsBadHeader = { + triggeringPrincipal: SYSTEMPRINCIPAL, + headers: {}, + }; + browser.webNavigation.loadURI( + Services.io.newURI(DUMMY1), + loadURIOptionsBadHeader + ); + ok(false, "Should have seen an exception from trying to pass some headers"); + } catch (e) { + ok(true, "Should have seen an exception from trying to pass some headers"); + } + + gBrowser.removeCurrentTab(); +}); diff --git a/toolkit/components/remotebrowserutils/tests/browser/browser_documentChannel.js b/toolkit/components/remotebrowserutils/tests/browser/browser_documentChannel.js new file mode 100644 index 0000000000..362bc9ee6d --- /dev/null +++ b/toolkit/components/remotebrowserutils/tests/browser/browser_documentChannel.js @@ -0,0 +1,280 @@ +/* eslint-env webextensions */ +"use strict"; + +const PRINT_POSTDATA = httpURL("print_postdata.sjs"); +const FILE_DUMMY = fileURL("dummy_page.html"); +const DATA_URL = "data:text/html,Hello%2C World!"; +const DATA_STRING = "Hello, World!"; + +async function performLoad(browser, opts, action) { + let loadedPromise = BrowserTestUtils.browserLoaded( + browser, + false, + opts.url, + opts.maybeErrorPage + ); + await action(); + await loadedPromise; +} + +const EXTENSION_DATA = { + manifest: { + name: "Simple extension test", + version: "1.0", + manifest_version: 2, + description: "", + + permissions: ["proxy", "webRequest", "webRequestBlocking", "<all_urls>"], + }, + + files: { + "dummy.html": "<html>webext dummy</html>", + "redirect.html": "<html>webext redirect</html>", + }, + + extUrl: "", + + async background() { + browser.test.log("background script running"); + browser.webRequest.onAuthRequired.addListener( + async details => { + browser.test.log("webRequest onAuthRequired"); + + // A blocking request that returns a promise exercises a codepath that + // sets the notificationCallbacks on the channel to a JS object that we + // can't do directly QueryObject on with expected results. + // This triggered a crash which was fixed in bug 1528188. + return new Promise((resolve, reject) => { + setTimeout(resolve, 0); + }); + }, + { urls: ["*://*/*"] }, + ["blocking"] + ); + browser.webRequest.onBeforeRequest.addListener( + async details => { + browser.test.log("webRequest onBeforeRequest"); + let isRedirect = + details.originUrl == browser.runtime.getURL("redirect.html") && + details.url.endsWith("print_postdata.sjs"); + let url = this.extUrl ? this.extUrl : details.url + "?redirected"; + return isRedirect ? { redirectUrl: url } : {}; + }, + { urls: ["*://*/*"] }, + ["blocking"] + ); + browser.test.onMessage.addListener(async ({ method, url }) => { + if (method == "setRedirectUrl") { + this.extUrl = url; + } + browser.test.sendMessage("done"); + }); + }, +}; + +async function withExtensionDummy(callback) { + let extension = ExtensionTestUtils.loadExtension(EXTENSION_DATA); + await extension.startup(); + let rv = await callback(`moz-extension://${extension.uuid}/`, extension); + await extension.unload(); + return rv; +} + +async function postFrom(start, target) { + return BrowserTestUtils.withNewTab( + { + gBrowser, + url: start, + }, + async function (browser) { + info("Test tab ready: postFrom " + start); + + // Create the form element in our loaded URI. + await SpecialPowers.spawn(browser, [{ target }], function ({ target }) { + // eslint-disable-next-line no-unsanitized/property + content.document.body.innerHTML = ` + <form method="post" action="${target}"> + <input type="text" name="initialRemoteType" value="${Services.appinfo.remoteType}"> + <input type="submit" id="submit"> + </form>`; + }); + + // Perform a form POST submit load. + info("Performing POST submission"); + await performLoad( + browser, + { + url(url) { + let enable = + url.startsWith(PRINT_POSTDATA) || + url == target || + url == DATA_URL; + if (!enable) { + info(`url ${url} is invalid to perform load`); + } + return enable; + }, + maybeErrorPage: true, + }, + async () => { + await SpecialPowers.spawn(browser, [], () => { + content.document.querySelector("#submit").click(); + }); + } + ); + + // Check that the POST data was submitted. + info("Fetching results"); + return SpecialPowers.spawn(browser, [], () => { + return { + remoteType: Services.appinfo.remoteType, + location: "" + content.location.href, + body: content.document.body.textContent, + }; + }); + } + ); +} + +async function loadAndGetProcessID(browser, target) { + info(`Performing GET load: ${target}`); + await performLoad( + browser, + { + maybeErrorPage: true, + }, + () => { + BrowserTestUtils.loadURIString(browser, target); + } + ); + + info(`Navigated to: ${target}`); + browser = gBrowser.selectedBrowser; + let processID = await SpecialPowers.spawn(browser, [], () => { + return Services.appinfo.processID; + }); + return processID; +} + +async function testLoadAndRedirect( + target, + expectedProcessSwitch, + testRedirect +) { + let start = httpURL(`dummy_page.html`); + return BrowserTestUtils.withNewTab( + { + gBrowser, + url: start, + }, + async function (_browser) { + info("Test tab ready: getFrom " + start); + + let browser = gBrowser.selectedBrowser; + let firstProcessID = await SpecialPowers.spawn(browser, [], () => { + return Services.appinfo.processID; + }); + + info(`firstProcessID: ${firstProcessID}`); + + let secondProcessID = await loadAndGetProcessID(browser, target); + + info(`secondProcessID: ${secondProcessID}`); + Assert.equal(firstProcessID != secondProcessID, expectedProcessSwitch); + + if (!testRedirect) { + return; + } + + let thirdProcessID = await loadAndGetProcessID(browser, add307(target)); + + info(`thirdProcessID: ${thirdProcessID}`); + Assert.equal(firstProcessID != thirdProcessID, expectedProcessSwitch); + Assert.ok(secondProcessID == thirdProcessID); + } + ); +} + +add_task(async function test_enabled() { + // Force only one webIsolated content process to ensure same-origin loads + // always end in the same process. + await SpecialPowers.pushPrefEnv({ + set: [["dom.ipc.processCount.webIsolated", 1]], + }); + + // URIs should correctly switch processes & the POST + // should succeed. + info("ENABLED -- FILE -- raw URI load"); + let resp = await postFrom(FILE_DUMMY, PRINT_POSTDATA); + ok(E10SUtils.isWebRemoteType(resp.remoteType), "process switch"); + is(resp.location, PRINT_POSTDATA, "correct location"); + is(resp.body, "initialRemoteType=file", "correct POST body"); + + info("ENABLED -- FILE -- 307-redirect URI load"); + let resp307 = await postFrom(FILE_DUMMY, add307(PRINT_POSTDATA)); + ok(E10SUtils.isWebRemoteType(resp307.remoteType), "process switch"); + is(resp307.location, PRINT_POSTDATA, "correct location"); + is(resp307.body, "initialRemoteType=file", "correct POST body"); + + // Same with extensions + await withExtensionDummy(async extOrigin => { + info("ENABLED -- EXTENSION -- raw URI load"); + let respExt = await postFrom(extOrigin + "dummy.html", PRINT_POSTDATA); + ok(E10SUtils.isWebRemoteType(respExt.remoteType), "process switch"); + is(respExt.location, PRINT_POSTDATA, "correct location"); + is(respExt.body, "initialRemoteType=extension", "correct POST body"); + + info("ENABLED -- EXTENSION -- extension-redirect URI load"); + let respExtRedirect = await postFrom( + extOrigin + "redirect.html", + PRINT_POSTDATA + ); + ok(E10SUtils.isWebRemoteType(respExtRedirect.remoteType), "process switch"); + is( + respExtRedirect.location, + PRINT_POSTDATA + "?redirected", + "correct location" + ); + is( + respExtRedirect.body, + "initialRemoteType=extension?redirected", + "correct POST body" + ); + + info("ENABLED -- EXTENSION -- 307-redirect URI load"); + let respExt307 = await postFrom( + extOrigin + "dummy.html", + add307(PRINT_POSTDATA) + ); + ok(E10SUtils.isWebRemoteType(respExt307.remoteType), "process switch"); + is(respExt307.location, PRINT_POSTDATA, "correct location"); + is(respExt307.body, "initialRemoteType=extension", "correct POST body"); + }); +}); + +async function sendMessage(ext, method, url) { + ext.sendMessage({ method, url }); + await ext.awaitMessage("done"); +} + +// TODO: Currently no test framework for ftp://. +add_task(async function test_protocol() { + // TODO: Processes should be switched due to navigation of different origins. + await testLoadAndRedirect("data:,foo", false, true); + + // Redirecting to file:// is not allowed. + await testLoadAndRedirect(FILE_DUMMY, true, false); + + await withExtensionDummy(async (extOrigin, extension) => { + await sendMessage(extension, "setRedirectUrl", DATA_URL); + + let respExtRedirect = await postFrom( + extOrigin + "redirect.html", + PRINT_POSTDATA + ); + + ok(E10SUtils.isWebRemoteType(respExtRedirect.remoteType), "process switch"); + is(respExtRedirect.location, DATA_URL, "correct location"); + is(respExtRedirect.body, DATA_STRING, "correct POST body"); + }); +}); diff --git a/toolkit/components/remotebrowserutils/tests/browser/browser_externalLinkBlanksPage.js b/toolkit/components/remotebrowserutils/tests/browser/browser_externalLinkBlanksPage.js new file mode 100644 index 0000000000..3cdfed48cc --- /dev/null +++ b/toolkit/components/remotebrowserutils/tests/browser/browser_externalLinkBlanksPage.js @@ -0,0 +1,79 @@ +/* + * Test that following a link with a scheme that opens externally (like + * irc:) does not blank the page (Bug 1630757). + */ + +const { HandlerServiceTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/HandlerServiceTestUtils.sys.mjs" +); + +let gHandlerService = Cc["@mozilla.org/uriloader/handler-service;1"].getService( + Ci.nsIHandlerService +); + +let Pages = [httpURL("dummy_page.html"), fileURL("dummy_page.html")]; + +/** + * Creates dummy protocol handler + */ +function initTestHandlers() { + let handlerInfo = + HandlerServiceTestUtils.getBlankHandlerInfo("test-proto://"); + + let appHandler = Cc[ + "@mozilla.org/uriloader/local-handler-app;1" + ].createInstance(Ci.nsILocalHandlerApp); + // This is a dir and not executable, but that's enough for here. + appHandler.executable = Services.dirsvc.get("XCurProcD", Ci.nsIFile); + handlerInfo.possibleApplicationHandlers.appendElement(appHandler); + handlerInfo.preferredApplicationHandler = appHandler; + handlerInfo.preferredAction = handlerInfo.useHelperApp; + handlerInfo.alwaysAskBeforeHandling = false; + gHandlerService.store(handlerInfo); + + registerCleanupFunction(() => { + gHandlerService.remove(handlerInfo); + }); +} + +async function runTest() { + initTestHandlers(); + + for (let page of Pages) { + await BrowserTestUtils.withNewTab(page, async function (aBrowser) { + await SpecialPowers.spawn(aBrowser, [], async () => { + let h = content.document.createElement("h1"); + ok(h); + h.innerHTML = "My heading"; + h.id = "my-heading"; + content.document.body.append(h); + is(content.document.getElementById("my-heading"), h, "h exists"); + + let a = content.document.createElement("a"); + ok(a); + a.innerHTML = "my link"; + a.id = "my-link"; + content.document.body.append(a); + }); + + await SpecialPowers.spawn(aBrowser, [], async () => { + let url = "test-proto://some-thing"; + + let a = content.document.getElementById("my-link"); + ok(a); + a.href = url; + a.click(); + }); + + await SpecialPowers.spawn(aBrowser, [], async () => { + ok( + content.document.getElementById("my-heading"), + "Page contents not erased" + ); + }); + }); + } + await SpecialPowers.popPrefEnv(); +} + +add_task(runTest); diff --git a/toolkit/components/remotebrowserutils/tests/browser/browser_httpCrossOriginOpenerPolicy.js b/toolkit/components/remotebrowserutils/tests/browser/browser_httpCrossOriginOpenerPolicy.js new file mode 100644 index 0000000000..a99db3b289 --- /dev/null +++ b/toolkit/components/remotebrowserutils/tests/browser/browser_httpCrossOriginOpenerPolicy.js @@ -0,0 +1,410 @@ +"use strict"; + +const COOP_PREF = "browser.tabs.remote.useCrossOriginOpenerPolicy"; + +async function setPref() { + await SpecialPowers.pushPrefEnv({ + set: [[COOP_PREF, true]], + }); +} + +async function unsetPref() { + await SpecialPowers.pushPrefEnv({ + set: [[COOP_PREF, false]], + }); +} + +function httpURL(filename, host = "https://example.com") { + let root = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + host + ); + return root + filename; +} + +async function performLoad(browser, opts, action) { + let loadedPromise = BrowserTestUtils.browserStopped( + browser, + opts.url, + opts.maybeErrorPage + ); + await action(); + await loadedPromise; +} + +async function test_coop( + start, + target, + expectedProcessSwitch, + startRemoteTypeCheck, + targetRemoteTypeCheck +) { + return BrowserTestUtils.withNewTab( + { + gBrowser, + url: start, + waitForStateStop: true, + }, + async function (_browser) { + info(`test_coop: Test tab ready: ${start}`); + + let browser = gBrowser.selectedBrowser; + let firstRemoteType = browser.remoteType; + let firstBC = browser.browsingContext; + + info(`firstBC: ${firstBC.id} remoteType: ${firstRemoteType}`); + + if (startRemoteTypeCheck) { + startRemoteTypeCheck(firstRemoteType); + } + + await performLoad( + browser, + { + url: target, + maybeErrorPage: false, + }, + async () => BrowserTestUtils.loadURIString(browser, target) + ); + + info(`Navigated to: ${target}`); + browser = gBrowser.selectedBrowser; + let secondRemoteType = browser.remoteType; + let secondBC = browser.browsingContext; + + info(`secondBC: ${secondBC.id} remoteType: ${secondRemoteType}`); + if (targetRemoteTypeCheck) { + targetRemoteTypeCheck(secondRemoteType); + } + if (expectedProcessSwitch) { + Assert.notEqual(firstBC.id, secondBC.id, `from: ${start} to ${target}`); + } else { + Assert.equal(firstBC.id, secondBC.id, `from: ${start} to ${target}`); + } + } + ); +} + +function waitForDownloadWindow() { + return new Promise(resolve => { + var listener = { + onOpenWindow: aXULWindow => { + info("Download window shown..."); + Services.wm.removeListener(listener); + + function downloadOnLoad() { + domwindow.removeEventListener("load", downloadOnLoad, true); + + is( + domwindow.document.location.href, + "chrome://mozapps/content/downloads/unknownContentType.xhtml", + "Download page appeared" + ); + resolve(domwindow); + } + + var domwindow = aXULWindow.docShell.domWindow; + domwindow.addEventListener("load", downloadOnLoad, true); + }, + onCloseWindow: aXULWindow => {}, + }; + + Services.wm.addListener(listener); + }); +} + +async function waitForDownloadUI() { + return BrowserTestUtils.waitForEvent(DownloadsPanel.panel, "popupshown"); +} + +async function cleanupDownloads(downloadList) { + info("cleaning up downloads"); + let [download] = await downloadList.getAll(); + await downloadList.remove(download); + await download.finalize(true); + + try { + if (Services.appinfo.OS === "WINNT") { + // We need to make the file writable to delete it on Windows. + await IOUtils.setPermissions(download.target.path, 0o600); + } + await IOUtils.remove(download.target.path); + } catch (error) { + info("The file " + download.target.path + " is not removed, " + error); + } + + if (DownloadsPanel.panel.state !== "closed") { + let hiddenPromise = BrowserTestUtils.waitForEvent( + DownloadsPanel.panel, + "popuphidden" + ); + DownloadsPanel.hidePanel(); + await hiddenPromise; + } + is( + DownloadsPanel.panel.state, + "closed", + "Check that the download panel is closed" + ); +} + +async function test_download_from(initCoop, downloadCoop) { + return BrowserTestUtils.withNewTab("about:blank", async function (_browser) { + info(`test_download: Test tab ready`); + + let start = httpURL( + "coop_header.sjs?downloadPage&coop=" + initCoop, + "https://example.com" + ); + await performLoad( + _browser, + { + url: start, + maybeErrorPage: false, + }, + async () => { + info(`test_download: Loading download page ${start}`); + return BrowserTestUtils.loadURIString(_browser, start); + } + ); + + info(`test_download: Download page ready ${start}`); + info(`Downloading ${downloadCoop}`); + + let expectDialog = Services.prefs.getBoolPref( + "browser.download.always_ask_before_handling_new_types", + false + ); + let resultPromise = expectDialog + ? waitForDownloadWindow() + : waitForDownloadUI(); + let browser = gBrowser.selectedBrowser; + SpecialPowers.spawn(browser, [downloadCoop], downloadCoop => { + content.document.getElementById(downloadCoop).click(); + }); + + // if the download page doesn't appear, the promise leads a timeout. + if (expectDialog) { + let win = await resultPromise; + await BrowserTestUtils.closeWindow(win); + } else { + // verify link target will get automatically downloaded + await resultPromise; + let downloadList = await Downloads.getList(Downloads.PUBLIC); + is((await downloadList.getAll()).length, 1, "Target was downloaded"); + await cleanupDownloads(downloadList); + is((await downloadList.getAll()).length, 0, "Downloads were cleaned up"); + } + }); +} + +// Check that multiple navigations of the same tab will only switch processes +// when it's expected. +add_task(async function test_multiple_nav_process_switches() { + await setPref(); + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: httpURL("coop_header.sjs", "https://example.org"), + waitForStateStop: true, + }, + async function (browser) { + let prevBC = browser.browsingContext; + + let target = httpURL("coop_header.sjs?.", "https://example.org"); + await performLoad( + browser, + { + url: target, + maybeErrorPage: false, + }, + async () => BrowserTestUtils.loadURIString(browser, target) + ); + + Assert.equal(prevBC, browser.browsingContext); + prevBC = browser.browsingContext; + + target = httpURL( + "coop_header.sjs?coop=same-origin", + "https://example.org" + ); + await performLoad( + browser, + { + url: target, + maybeErrorPage: false, + }, + async () => BrowserTestUtils.loadURIString(browser, target) + ); + + Assert.notEqual(prevBC, browser.browsingContext); + prevBC = browser.browsingContext; + + target = httpURL( + "coop_header.sjs?coop=same-origin", + "https://example.com" + ); + await performLoad( + browser, + { + url: target, + maybeErrorPage: false, + }, + async () => BrowserTestUtils.loadURIString(browser, target) + ); + + Assert.notEqual(prevBC, browser.browsingContext); + prevBC = browser.browsingContext; + + target = httpURL( + "coop_header.sjs?coop=same-origin&index=4", + "https://example.com" + ); + await performLoad( + browser, + { + url: target, + maybeErrorPage: false, + }, + async () => BrowserTestUtils.loadURIString(browser, target) + ); + + Assert.equal(prevBC, browser.browsingContext); + } + ); +}); + +add_task(async function test_disabled() { + await unsetPref(); + await test_coop( + httpURL("coop_header.sjs", "https://example.com"), + httpURL("coop_header.sjs", "https://example.com"), + false + ); + await test_coop( + httpURL("coop_header.sjs?coop=same-origin", "http://example.com"), + httpURL("coop_header.sjs", "http://example.com"), + false + ); + await test_coop( + httpURL("coop_header.sjs", "http://example.com"), + httpURL("coop_header.sjs?coop=same-origin", "http://example.com"), + false + ); +}); + +add_task(async function test_enabled() { + await setPref(); + + function checkIsCoopRemoteType(remoteType) { + Assert.ok( + remoteType.startsWith(E10SUtils.WEB_REMOTE_COOP_COEP_TYPE_PREFIX), + `${remoteType} expected to be coop` + ); + } + + function checkIsNotCoopRemoteType(remoteType) { + if (gFissionBrowser) { + Assert.ok( + remoteType.startsWith("webIsolated="), + `${remoteType} expected to start with webIsolated=` + ); + } else { + Assert.equal( + remoteType, + E10SUtils.WEB_REMOTE_TYPE, + `${remoteType} expected to be web` + ); + } + } + + await test_coop( + httpURL("coop_header.sjs", "https://example.com"), + httpURL("coop_header.sjs", "https://example.com"), + false, + checkIsNotCoopRemoteType, + checkIsNotCoopRemoteType + ); + await test_coop( + httpURL("coop_header.sjs", "https://example.com"), + httpURL("coop_header.sjs?coop=same-origin", "https://example.org"), + true, + checkIsNotCoopRemoteType, + checkIsNotCoopRemoteType + ); + await test_coop( + httpURL("coop_header.sjs?coop=same-origin&index=1", "https://example.com"), + httpURL("coop_header.sjs?coop=same-origin&index=1", "https://example.org"), + true, + checkIsNotCoopRemoteType, + checkIsNotCoopRemoteType + ); + await test_coop( + httpURL("coop_header.sjs", "https://example.com"), + httpURL( + "coop_header.sjs?coop=same-origin&coep=require-corp", + "https://example.com" + ), + true, + checkIsNotCoopRemoteType, + checkIsCoopRemoteType + ); + await test_coop( + httpURL( + "coop_header.sjs?coop=same-origin&coep=require-corp&index=2", + "https://example.com" + ), + httpURL( + "coop_header.sjs?coop=same-origin&coep=require-corp&index=3", + "https://example.com" + ), + false, + checkIsCoopRemoteType, + checkIsCoopRemoteType + ); + await test_coop( + httpURL( + "coop_header.sjs?coop=same-origin&coep=require-corp&index=4", + "https://example.com" + ), + httpURL("coop_header.sjs", "https://example.com"), + true, + checkIsCoopRemoteType, + checkIsNotCoopRemoteType + ); + await test_coop( + httpURL( + "coop_header.sjs?coop=same-origin&coep=require-corp&index=5", + "https://example.com" + ), + httpURL( + "coop_header.sjs?coop=same-origin&coep=require-corp&index=6", + "https://example.org" + ), + true, + checkIsCoopRemoteType, + checkIsCoopRemoteType + ); +}); + +add_task(async function test_download() { + requestLongerTimeout(4); + await setPref(); + + let initCoopArray = ["", "same-origin"]; + + let downloadCoopArray = [ + "no-coop", + "same-origin", + "same-origin-allow-popups", + ]; + + // If the coop mismatch between current page and download link, clicking the + // download link will make the page empty and popup the download window. That + // forces us to reload the page every time. + for (var initCoop of initCoopArray) { + for (var downloadCoop of downloadCoopArray) { + await test_download_from(initCoop, downloadCoop); + } + } +}); diff --git a/toolkit/components/remotebrowserutils/tests/browser/browser_httpToFileHistory.js b/toolkit/components/remotebrowserutils/tests/browser/browser_httpToFileHistory.js new file mode 100644 index 0000000000..56151ae94e --- /dev/null +++ b/toolkit/components/remotebrowserutils/tests/browser/browser_httpToFileHistory.js @@ -0,0 +1,115 @@ +const HISTORY = [ + { url: httpURL("dummy_page.html") }, + { url: fileURL("dummy_page.html") }, + { url: httpURL("dummy_page.html") }, +]; + +function reversed(list) { + let copy = list.slice(); + copy.reverse(); + return copy; +} + +function butLast(list) { + return list.slice(0, -1); +} + +async function runTest() { + await BrowserTestUtils.withNewTab({ gBrowser }, async function (aBrowser) { + // Perform initial load of each URL in the history. + let count = 0; + let index = -1; + for (let { url } of HISTORY) { + BrowserTestUtils.loadURIString(aBrowser, url); + + await BrowserTestUtils.browserLoaded(aBrowser, false, loaded => { + return ( + Services.io.newURI(loaded).scheme == Services.io.newURI(url).scheme + ); + }); + + count++; + index++; + await SpecialPowers.spawn( + aBrowser, + [{ count, index, url }], + async function ({ count, index, url }) { + docShell.QueryInterface(Ci.nsIWebNavigation); + + is( + docShell.sessionHistory.count, + count, + "Initial Navigation Count Match" + ); + is( + docShell.sessionHistory.index, + index, + "Initial Navigation Index Match" + ); + + let real = Services.io.newURI(content.location.href); + let expect = Services.io.newURI(url); + is(real.scheme, expect.scheme, "Initial Navigation URL Scheme"); + } + ); + } + + // Go back to the first entry. + for (let { url } of reversed(HISTORY).slice(1)) { + SpecialPowers.spawn(aBrowser, [], () => { + content.history.back(); + }); + await BrowserTestUtils.browserLoaded(aBrowser, false, loaded => { + return ( + Services.io.newURI(loaded).scheme == Services.io.newURI(url).scheme + ); + }); + + index--; + await SpecialPowers.spawn( + aBrowser, + [{ count, index, url }], + async function ({ count, index, url }) { + docShell.QueryInterface(Ci.nsIWebNavigation); + + is(docShell.sessionHistory.count, count, "Go Back Count Match"); + is(docShell.sessionHistory.index, index, "Go Back Index Match"); + + let real = Services.io.newURI(content.location.href); + let expect = Services.io.newURI(url); + is(real.scheme, expect.scheme, "Go Back URL Scheme"); + } + ); + } + + // Go forward to the last entry. + for (let { url } of HISTORY.slice(1)) { + SpecialPowers.spawn(aBrowser, [], () => { + content.history.forward(); + }); + await BrowserTestUtils.browserLoaded(aBrowser, false, loaded => { + return ( + Services.io.newURI(loaded).scheme == Services.io.newURI(url).scheme + ); + }); + + index++; + await SpecialPowers.spawn( + aBrowser, + [{ count, index, url }], + async function ({ count, index, url }) { + docShell.QueryInterface(Ci.nsIWebNavigation); + + is(docShell.sessionHistory.count, count, "Go Forward Count Match"); + is(docShell.sessionHistory.index, index, "Go Forward Index Match"); + + let real = Services.io.newURI(content.location.href); + let expect = Services.io.newURI(url); + is(real.scheme, expect.scheme, "Go Forward URL Scheme"); + } + ); + } + }); +} + +add_task(runTest); diff --git a/toolkit/components/remotebrowserutils/tests/browser/browser_oopProcessSwap.js b/toolkit/components/remotebrowserutils/tests/browser/browser_oopProcessSwap.js new file mode 100644 index 0000000000..3286227d37 --- /dev/null +++ b/toolkit/components/remotebrowserutils/tests/browser/browser_oopProcessSwap.js @@ -0,0 +1,161 @@ +add_task(async function oopProcessSwap() { + const FILE = fileURL("dummy_page.html"); + const WEB = httpURL("file_postmsg_parent.html"); + + let win = await BrowserTestUtils.openNewBrowserWindow({ fission: true }); + + await BrowserTestUtils.withNewTab( + { gBrowser: win.gBrowser, url: FILE }, + async browser => { + is(browser.browsingContext.children.length, 0); + + info("creating an in-process frame"); + let frameId = await SpecialPowers.spawn( + browser, + [{ FILE }], + async ({ FILE }) => { + let iframe = content.document.createElement("iframe"); + iframe.setAttribute("src", FILE); + content.document.body.appendChild(iframe); + + // The nested URI should be same-process + ok(iframe.browsingContext.docShell, "Should be in-process"); + + return iframe.browsingContext.id; + } + ); + + is(browser.browsingContext.children.length, 1); + + info("navigating to x-process frame"); + let oopinfo = await SpecialPowers.spawn( + browser, + [{ WEB }], + async ({ WEB }) => { + let iframe = content.document.querySelector("iframe"); + + iframe.contentWindow.location = WEB; + + let data = await new Promise(resolve => { + content.window.addEventListener( + "message", + function (evt) { + info("oop iframe loaded"); + is(evt.source, iframe.contentWindow); + resolve(evt.data); + }, + { once: true } + ); + }); + + is(iframe.browsingContext.docShell, null, "Should be out-of-process"); + is( + iframe.browsingContext.embedderElement, + iframe, + "correct embedder" + ); + + return { + location: data.location, + browsingContextId: iframe.browsingContext.id, + }; + } + ); + + is(browser.browsingContext.children.length, 1); + + if (Services.prefs.getBoolPref("fission.preserve_browsing_contexts")) { + is( + frameId, + oopinfo.browsingContextId, + `BrowsingContext should not have changed (${frameId} != ${oopinfo.browsingContextId})` + ); + } + is(oopinfo.location, WEB, "correct location"); + } + ); + + await BrowserTestUtils.closeWindow(win); +}); + +add_task(async function oopOriginProcessSwap() { + const COM_DUMMY = httpURL("dummy_page.html", "https://example.com/"); + const ORG_POSTMSG = httpURL( + "file_postmsg_parent.html", + "https://example.org/" + ); + + let win = await BrowserTestUtils.openNewBrowserWindow({ fission: true }); + + await BrowserTestUtils.withNewTab( + { gBrowser: win.gBrowser, url: COM_DUMMY }, + async browser => { + is(browser.browsingContext.children.length, 0); + + info("creating an in-process frame"); + let frameId = await SpecialPowers.spawn( + browser, + [{ COM_DUMMY }], + async ({ COM_DUMMY }) => { + let iframe = content.document.createElement("iframe"); + iframe.setAttribute("src", COM_DUMMY); + content.document.body.appendChild(iframe); + + // The nested URI should be same-process + ok(iframe.browsingContext.docShell, "Should be in-process"); + + return iframe.browsingContext.id; + } + ); + + is(browser.browsingContext.children.length, 1); + + info("navigating to x-process frame"); + let oopinfo = await SpecialPowers.spawn( + browser, + [{ ORG_POSTMSG }], + async ({ ORG_POSTMSG }) => { + let iframe = content.document.querySelector("iframe"); + + iframe.contentWindow.location = ORG_POSTMSG; + + let data = await new Promise(resolve => { + content.window.addEventListener( + "message", + function (evt) { + info("oop iframe loaded"); + is(evt.source, iframe.contentWindow); + resolve(evt.data); + }, + { once: true } + ); + }); + + is(iframe.browsingContext.docShell, null, "Should be out-of-process"); + is( + iframe.browsingContext.embedderElement, + iframe, + "correct embedder" + ); + + return { + location: data.location, + browsingContextId: iframe.browsingContext.id, + }; + } + ); + + is(browser.browsingContext.children.length, 1); + if (Services.prefs.getBoolPref("fission.preserve_browsing_contexts")) { + is( + frameId, + oopinfo.browsingContextId, + `BrowsingContext should not have changed (${frameId} != ${oopinfo.browsingContextId})` + ); + } + is(oopinfo.location, ORG_POSTMSG, "correct location"); + } + ); + + await BrowserTestUtils.closeWindow(win); +}); diff --git a/toolkit/components/remotebrowserutils/tests/browser/coop_header.sjs b/toolkit/components/remotebrowserutils/tests/browser/coop_header.sjs new file mode 100644 index 0000000000..c6b537d770 --- /dev/null +++ b/toolkit/components/remotebrowserutils/tests/browser/coop_header.sjs @@ -0,0 +1,58 @@ +function handleRequest(request, response) { + Cu.importGlobalProperties(["URLSearchParams"]); + let query = new URLSearchParams(request.queryString); + + response.setStatusLine(request.httpVersion, 200, "OK"); + + // The tests for Cross-Origin-Opener-Policy unfortunately depend on + // BFCacheInParent not kicking in, as with that enabled, it is not possible to + // tell whether the BrowsingContext switch was caused by the BFCache + // navigation or by the COOP mismatch. This header disables BFCache for the + // coop documents, and should avoid the issue. + response.setHeader("Cache-Control", "no-store", false); + + let isDownloadPage = false; + let isDownloadFile = false; + + query.forEach((value, name) => { + if (name === "downloadPage") { + isDownloadPage = true; + } else if (name === "downloadFile") { + isDownloadFile = true; + } else if (name == "coop") { + response.setHeader("Cross-Origin-Opener-Policy", unescape(value), false); + } else if (name == "coep") { + response.setHeader( + "Cross-Origin-Embedder-Policy", + unescape(value), + false + ); + } + }); + + let downloadHTML = ""; + if (isDownloadPage) { + ["no-coop", "same-origin", "same-origin-allow-popups"].forEach(coop => { + downloadHTML += + '<a href="https://example.com/browser/toolkit/components/remotebrowserutils/tests/browser/coop_header.sjs?downloadFile&' + + (coop === "no-coop" ? "" : coop) + + '" id="' + + coop + + '" download>' + + unescape(coop) + + "</a> <br>"; + }); + } + + if (isDownloadFile) { + response.setHeader("Content-Type", "application/octet-stream", false); + response.write("BINARY_DATA"); + } else { + response.setHeader("Content-Type", "text/html; charset=utf-8", false); + response.write( + "<!DOCTYPE html><html><body><p>Hello world</p> " + + downloadHTML + + "</body></html>" + ); + } +} diff --git a/toolkit/components/remotebrowserutils/tests/browser/dummy_page.html b/toolkit/components/remotebrowserutils/tests/browser/dummy_page.html new file mode 100644 index 0000000000..8205e90d5d --- /dev/null +++ b/toolkit/components/remotebrowserutils/tests/browser/dummy_page.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<meta charset="utf-8"> + +<html> +<body> +<p>Page</p> + <script> + // Prevent this page from being stored in the bfcache for the + // browser_httpToFileHistory.js test + window.blockBFCache = new RTCPeerConnection(); + </script> +</body> +</html> diff --git a/toolkit/components/remotebrowserutils/tests/browser/file_postmsg_parent.html b/toolkit/components/remotebrowserutils/tests/browser/file_postmsg_parent.html new file mode 100644 index 0000000000..d5f640775b --- /dev/null +++ b/toolkit/components/remotebrowserutils/tests/browser/file_postmsg_parent.html @@ -0,0 +1,5 @@ +<!DOCTYPE html> +<html> +<body onload="parent.postMessage({location: window.location.href}, '*')"> +</body> +</html> diff --git a/toolkit/components/remotebrowserutils/tests/browser/head.js b/toolkit/components/remotebrowserutils/tests/browser/head.js new file mode 100644 index 0000000000..ffdd9375cf --- /dev/null +++ b/toolkit/components/remotebrowserutils/tests/browser/head.js @@ -0,0 +1,17 @@ +function fileURL(filename) { + let ifile = getChromeDir(getResolvedURI(gTestPath)); + ifile.append(filename); + return Services.io.newFileURI(ifile).spec; +} + +function httpURL(filename, host = "https://example.com/") { + let root = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content/", + host + ); + return root + filename; +} + +function add307(url, host = "https://example.com/") { + return httpURL("307redirect.sjs?" + url, host); +} diff --git a/toolkit/components/remotebrowserutils/tests/browser/print_postdata.sjs b/toolkit/components/remotebrowserutils/tests/browser/print_postdata.sjs new file mode 100644 index 0000000000..bd9c18f7ea --- /dev/null +++ b/toolkit/components/remotebrowserutils/tests/browser/print_postdata.sjs @@ -0,0 +1,28 @@ +const CC = Components.Constructor; +const BinaryInputStream = CC( + "@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream" +); + +function handleRequest(request, response) { + response.setHeader("Content-Type", "text/plain", false); + if (request.method == "GET") { + response.write(request.queryString); + } else { + var body = new BinaryInputStream(request.bodyInputStream); + + var avail; + var bytes = []; + + while ((avail = body.available()) > 0) { + Array.prototype.push.apply(bytes, body.readByteArray(avail)); + } + + var data = String.fromCharCode.apply(null, bytes); + if (request.queryString) { + data = data + "?" + request.queryString; + } + response.bodyOutputStream.write(data, data.length); + } +} |