diff options
Diffstat (limited to '')
7 files changed, 578 insertions, 0 deletions
diff --git a/toolkit/mozapps/notificationserver/NotificationCallback.cpp b/toolkit/mozapps/notificationserver/NotificationCallback.cpp new file mode 100644 index 0000000000..01cce33622 --- /dev/null +++ b/toolkit/mozapps/notificationserver/NotificationCallback.cpp @@ -0,0 +1,271 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* 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 "NotificationCallback.h" + +#include <sstream> +#include <string> +#include <vector> + +#include "mozilla/CmdLineAndEnvUtils.h" +#include "mozilla/ToastNotificationHeaderOnlyUtils.h" + +using namespace mozilla::widget::toastnotification; + +HRESULT STDMETHODCALLTYPE +NotificationCallback::QueryInterface(REFIID riid, void** ppvObject) { + if (!ppvObject) { + return E_POINTER; + } + + *ppvObject = nullptr; + + if (!(riid == guid || riid == __uuidof(INotificationActivationCallback) || + riid == __uuidof(IUnknown))) { + return E_NOINTERFACE; + } + + AddRef(); + *ppvObject = reinterpret_cast<void*>(this); + + return S_OK; +} + +HRESULT STDMETHODCALLTYPE NotificationCallback::Activate( + LPCWSTR appUserModelId, LPCWSTR invokedArgs, + const NOTIFICATION_USER_INPUT_DATA* data, ULONG dataCount) { + HandleActivation(invokedArgs); + + // Windows 8 style callbacks are not called and notifications are not removed + // from the Action Center unless we return `S_OK`, so always do so even if + // we're unable to handle the notification properly. + return S_OK; +} + +void NotificationCallback::HandleActivation(LPCWSTR invokedArgs) { + auto maybeArgs = ParseToastArguments(invokedArgs); + if (maybeArgs) { + NOTIFY_LOG(mozilla::LogLevel::Info, + (L"Invoked with arguments: '%s'", invokedArgs)); + } else { + NOTIFY_LOG(mozilla::LogLevel::Info, (L"COM server disabled for toast")); + return; + } + const auto& args = maybeArgs.value(); + auto [programPath, cmdLine] = BuildRunCommand(args); + + // This pipe object will let Firefox notify us when it has handled the + // notification. Create this before interacting with the application so the + // application can rely on it existing. + auto maybePipe = CreatePipe(args.windowsTag); + + // Run the application. + + STARTUPINFOW si = {}; + si.cb = sizeof(STARTUPINFOW); + PROCESS_INFORMATION pi = {}; + + // Runs `{program path} [--profile {profile path}] [--notification-windowsTag + // {tag}]`. + CreateProcessW(programPath.c_str(), cmdLine.get(), nullptr, nullptr, false, + DETACHED_PROCESS | NORMAL_PRIORITY_CLASS, nullptr, nullptr, + &si, &pi); + + NOTIFY_LOG(mozilla::LogLevel::Info, (L"Invoked %s", cmdLine.get())); + + // Transfer `SetForegroundWindow` permission to the launched application. + + maybePipe.apply([](const auto& pipe) { + if (ConnectPipeWithTimeout(pipe)) { + HandlePipeMessages(pipe); + } + }); +} + +mozilla::Maybe<ToastArgs> NotificationCallback::ParseToastArguments( + LPCWSTR invokedArgs) { + ToastArgs parsedArgs; + std::wistringstream args(invokedArgs); + bool serverDisabled = true; + + for (std::wstring key, value; + std::getline(args, key) && std::getline(args, value);) { + if (key == kLaunchArgProgram) { + serverDisabled = false; + } else if (key == kLaunchArgProfile) { + parsedArgs.profile = value; + } else if (key == kLaunchArgTag) { + parsedArgs.windowsTag = value; + } else if (key == kLaunchArgLogging) { + gVerbose = value == L"verbose"; + } else if (key == kLaunchArgAction) { + // Remainder of args are from the Web Notification action, don't parse. + // See https://bugzilla.mozilla.org/show_bug.cgi?id=1781929. + break; + } + } + + if (serverDisabled) { + return mozilla::Nothing(); + } + + return mozilla::Some(parsedArgs); +} + +std::tuple<path, mozilla::UniquePtr<wchar_t[]>> +NotificationCallback::BuildRunCommand(const ToastArgs& args) { + path programPath = installDir / L"" MOZ_APP_NAME; + programPath += L".exe"; + + std::vector<const wchar_t*> childArgv; + childArgv.push_back(programPath.c_str()); + + if (!args.profile.empty()) { + childArgv.push_back(L"--profile"); + childArgv.push_back(args.profile.c_str()); + } else { + NOTIFY_LOG(mozilla::LogLevel::Warning, + (L"No profile; invocation will choose default profile")); + } + + if (!args.windowsTag.empty()) { + childArgv.push_back(L"--notification-windowsTag"); + childArgv.push_back(args.windowsTag.c_str()); + } else { + NOTIFY_LOG(mozilla::LogLevel::Warning, (L"No windowsTag; invoking anyway")); + } + + return {programPath, + mozilla::MakeCommandLine(childArgv.size(), childArgv.data())}; +} + +mozilla::Maybe<nsAutoHandle> NotificationCallback::CreatePipe( + const std::wstring& tag) { + if (tag.empty()) { + return mozilla::Nothing(); + } + + // Prefix required by pipe API. + std::wstring pipeName = GetNotificationPipeName(tag.c_str()); + + nsAutoHandle pipe(CreateNamedPipeW( + pipeName.c_str(), PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED, + PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT | + PIPE_REJECT_REMOTE_CLIENTS, + 1, sizeof(ToastNotificationPermissionMessage), + sizeof(ToastNotificationPidMessage), 0, nullptr)); + if (pipe.get() == INVALID_HANDLE_VALUE) { + NOTIFY_LOG(mozilla::LogLevel::Error, (L"Error creating pipe %s, error %lu", + pipeName.c_str(), GetLastError())); + return mozilla::Nothing(); + } + + return mozilla::Some(pipe.out()); +} + +bool NotificationCallback::ConnectPipeWithTimeout(const nsAutoHandle& pipe) { + nsAutoHandle overlappedEvent(CreateEventW(nullptr, TRUE, FALSE, nullptr)); + if (!overlappedEvent) { + NOTIFY_LOG( + mozilla::LogLevel::Error, + (L"Error creating pipe connect event, error %lu", GetLastError())); + return false; + } + + OVERLAPPED overlappedConnect{}; + overlappedConnect.hEvent = overlappedEvent.get(); + + BOOL result = ConnectNamedPipe(pipe.get(), &overlappedConnect); + DWORD lastError = GetLastError(); + if (lastError == ERROR_IO_PENDING) { + NOTIFY_LOG(mozilla::LogLevel::Info, (L"Waiting on pipe connection")); + + if (!WaitEventWithTimeout(overlappedEvent)) { + NOTIFY_LOG(mozilla::LogLevel::Warning, + (L"Pipe connect wait failed, cancelling (connection may still " + L"succeed)")); + + CancelIo(pipe.get()); + DWORD undefined; + BOOL overlappedResult = + GetOverlappedResult(pipe.get(), &overlappedConnect, &undefined, TRUE); + if (!overlappedResult || GetLastError() != ERROR_PIPE_CONNECTED) { + NOTIFY_LOG(mozilla::LogLevel::Error, + (L"Pipe connect failed, error %lu", GetLastError())); + return false; + } + + // Pipe connected before cancellation, fall through. + } + } else if (result) { + // Overlapped `ConnectNamedPipe` should return 0. + NOTIFY_LOG(mozilla::LogLevel::Error, + (L"Error connecting pipe, error %lu", lastError)); + return false; + } else if (lastError != ERROR_PIPE_CONNECTED) { + NOTIFY_LOG(mozilla::LogLevel::Error, + (L"Error connecting pipe, error %lu", lastError)); + return false; + } + + NOTIFY_LOG(mozilla::LogLevel::Info, (L"Pipe connected!")); + return true; +} + +void NotificationCallback::HandlePipeMessages(const nsAutoHandle& pipe) { + ToastNotificationPidMessage in{}; + auto read = [&](OVERLAPPED& overlapped) { + return ReadFile(pipe.get(), &in, sizeof(in), nullptr, &overlapped); + }; + if (!SyncDoOverlappedIOWithTimeout(pipe, sizeof(in), read)) { + NOTIFY_LOG(mozilla::LogLevel::Error, (L"Pipe read failed")); + return; + } + + ToastNotificationPermissionMessage out{}; + out.setForegroundPermissionGranted = TransferForegroundPermission(in.pid); + auto write = [&](OVERLAPPED& overlapped) { + return WriteFile(pipe.get(), &out, sizeof(out), nullptr, &overlapped); + }; + if (!SyncDoOverlappedIOWithTimeout(pipe, sizeof(out), write)) { + NOTIFY_LOG(mozilla::LogLevel::Error, (L"Pipe write failed")); + return; + } + + NOTIFY_LOG(mozilla::LogLevel::Info, (L"Pipe write succeeded!")); +} + +DWORD NotificationCallback::TransferForegroundPermission(DWORD pid) { + // When the instance of Firefox is still running we need to grant it + // foreground permission to bring itself to the foreground. We're able to do + // this even though the COM server is not the foreground process likely due to + // Windows granting permission to the COM object via + // `CoAllowSetForegroundWindow`. + // + // Note that issues surrounding `SetForegroundWindow` permissions are obscured + // when builds are run with a debugger, whereupon Windows grants + // `SetForegroundWindow` permission in all instances. + // + // We can not rely on granting this permission to the process created above + // because remote server clients do not meet the criteria to receive + // `SetForegroundWindow` permissions without unsupported hacks. + if (!pid) { + NOTIFY_LOG(mozilla::LogLevel::Warning, + (L"`pid` received from pipe was 0, no process to grant " + L"`SetForegroundWindow` permission to")); + return FALSE; + } + // When this call succeeds, the COM process loses the `SetForegroundWindow` + // permission. + if (!AllowSetForegroundWindow(pid)) { + NOTIFY_LOG(mozilla::LogLevel::Error, + (L"Failed to grant `SetForegroundWindow` permission, error %lu", + GetLastError())); + return FALSE; + } + + return TRUE; +} diff --git a/toolkit/mozapps/notificationserver/NotificationCallback.h b/toolkit/mozapps/notificationserver/NotificationCallback.h new file mode 100644 index 0000000000..3fcb50a812 --- /dev/null +++ b/toolkit/mozapps/notificationserver/NotificationCallback.h @@ -0,0 +1,72 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* 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 NotificationCallback_h__ +#define NotificationCallback_h__ + +#include <filesystem> +#include <tuple> +#include <unknwn.h> +#include <wrl.h> + +#include "mozilla/Maybe.h" +#include "nsWindowsHelpers.h" + +using namespace Microsoft::WRL; +using namespace std::filesystem; + +// Windows 10+ declarations. +// TODO remove declarations and add `#include +// <notificationactivationcallback.h>` when Windows 10 is the minimum supported. +typedef struct NOTIFICATION_USER_INPUT_DATA { + LPCWSTR Key; + LPCWSTR Value; +} NOTIFICATION_USER_INPUT_DATA; + +MIDL_INTERFACE("53E31837-6600-4A81-9395-75CFFE746F94") +INotificationActivationCallback : public IUnknown { + public: + virtual HRESULT STDMETHODCALLTYPE Activate( + LPCWSTR appUserModelId, LPCWSTR invokedArgs, + const NOTIFICATION_USER_INPUT_DATA* data, ULONG count) = 0; +}; + +struct ToastArgs { + std::wstring profile; + std::wstring windowsTag; +}; + +class NotificationCallback final + : public RuntimeClass<RuntimeClassFlags<ClassicCom>, + INotificationActivationCallback> { + public: + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppvObject) final; + + HRESULT STDMETHODCALLTYPE Activate(LPCWSTR appUserModelId, + LPCWSTR invokedArgs, + const NOTIFICATION_USER_INPUT_DATA* data, + ULONG dataCount) final; + + explicit NotificationCallback(const GUID& runtimeGuid, + const path& dllInstallDir) + : guid(runtimeGuid), installDir(dllInstallDir) {} + + private: + const GUID guid = {}; + const path installDir = {}; + + void HandleActivation(LPCWSTR invokedArgs); + mozilla::Maybe<ToastArgs> ParseToastArguments(LPCWSTR invokedArgs); + std::tuple<path, mozilla::UniquePtr<wchar_t[]>> BuildRunCommand( + const ToastArgs& args); + + static mozilla::Maybe<nsAutoHandle> CreatePipe(const std::wstring& tag); + static bool ConnectPipeWithTimeout(const nsAutoHandle& pipe); + static void HandlePipeMessages(const nsAutoHandle& pipe); + static DWORD TransferForegroundPermission(const DWORD pid); +}; + +#endif diff --git a/toolkit/mozapps/notificationserver/NotificationComServer.cpp b/toolkit/mozapps/notificationserver/NotificationComServer.cpp new file mode 100644 index 0000000000..184b55be44 --- /dev/null +++ b/toolkit/mozapps/notificationserver/NotificationComServer.cpp @@ -0,0 +1,132 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* 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 <filesystem> +#include <string> + +#include "mozilla/WinHeaderOnlyUtils.h" + +#include "NotificationFactory.h" + +using namespace std::filesystem; + +static path processDllPath = {}; + +// Populate the path to this DLL. +bool PopulateDllPath(HINSTANCE dllInstance) { + std::vector<wchar_t> path(MAX_PATH, 0); + DWORD charsWritten = + GetModuleFileNameW(dllInstance, path.data(), path.size()); + + // GetModuleFileNameW returns the count of characters written including null + // when truncated, excluding null otherwise. Therefore the count will always + // be less than the buffer size when not truncated. + while (charsWritten == path.size()) { + path.resize(path.size() * 2, 0); + charsWritten = GetModuleFileNameW(dllInstance, path.data(), path.size()); + } + + if (charsWritten == 0) { + return false; + } + + processDllPath = path.data(); + return true; +} + +// Our activator's CLSID is generated once either during install or at runtime +// by the application generating the notification so that notifications work +// with parallel installs and portable/development builds. When a COM object is +// requested we verify the CLSID's InprocServer registry entry matches this +// DLL's path. +bool CheckRuntimeClsid(REFCLSID rclsid) { + // MSIX Notification COM Server registration is isolated to the package and is + // identical across installs/channels. + if (mozilla::HasPackageIdentity()) { + // Keep synchronized with `python\mozbuild\mozbuild\repackaging\msix.py`. + constexpr CLSID MOZ_INOTIFICATIONACTIVATION_CLSID = { + 0x916f9b5d, + 0xb5b2, + 0x4d36, + {0xb0, 0x47, 0x03, 0xc7, 0xa5, 0x2f, 0x81, 0xc8}}; + + return IsEqualCLSID(rclsid, MOZ_INOTIFICATIONACTIVATION_CLSID); + } + + std::wstring clsid_str; + { + wchar_t* raw_clsid_str; + if (SUCCEEDED(StringFromCLSID(rclsid, &raw_clsid_str))) { + clsid_str += raw_clsid_str; + CoTaskMemFree(raw_clsid_str); + } else { + return false; + } + } + + std::wstring key = L"CLSID\\"; + key += clsid_str; + key += L"\\InprocServer32"; + + DWORD bufferLen = 0; + LSTATUS status = RegGetValueW(HKEY_CLASSES_ROOT, key.c_str(), L"", + RRF_RT_REG_SZ, nullptr, nullptr, &bufferLen); + if (status != ERROR_SUCCESS) { + return false; + } + + std::vector<wchar_t> clsidDllPathBuffer(bufferLen / sizeof(wchar_t)); + // Sanity assignment in case the buffer length found was not an integer + // multiple of `sizeof(wchar_t)`. + bufferLen = clsidDllPathBuffer.size() * sizeof(wchar_t); + + status = RegGetValueW(HKEY_CLASSES_ROOT, key.c_str(), L"", RRF_RT_REG_SZ, + nullptr, clsidDllPathBuffer.data(), &bufferLen); + if (status != ERROR_SUCCESS) { + return false; + } + + path clsidDllPath = clsidDllPathBuffer.data(); + return equivalent(processDllPath, clsidDllPath); +} + +extern "C" { +HRESULT STDMETHODCALLTYPE DllGetClassObject(REFCLSID rclsid, REFIID riid, + LPVOID* ppv) { + if (!ppv) { + return E_INVALIDARG; + } + *ppv = nullptr; + + if (!CheckRuntimeClsid(rclsid)) { + return CLASS_E_CLASSNOTAVAILABLE; + } + + using namespace Microsoft::WRL; + ComPtr<NotificationFactory> factory = + Make<NotificationFactory, const GUID&, const path&>( + rclsid, processDllPath.parent_path()); + + switch (factory->QueryInterface(riid, ppv)) { + case S_OK: + return S_OK; + case E_NOINTERFACE: + return CLASS_E_CLASSNOTAVAILABLE; + default: + return E_UNEXPECTED; + } +} + +BOOL STDMETHODCALLTYPE DllMain(HINSTANCE hinstDLL, DWORD fdwReason, + LPVOID lpReserved) { + if (fdwReason == DLL_PROCESS_ATTACH) { + if (!PopulateDllPath(hinstDLL)) { + return FALSE; + } + } + return TRUE; +} +} diff --git a/toolkit/mozapps/notificationserver/NotificationFactory.cpp b/toolkit/mozapps/notificationserver/NotificationFactory.cpp new file mode 100644 index 0000000000..a1043f17b9 --- /dev/null +++ b/toolkit/mozapps/notificationserver/NotificationFactory.cpp @@ -0,0 +1,33 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* 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 "NotificationFactory.h" + +HRESULT STDMETHODCALLTYPE NotificationFactory::CreateInstance( + IUnknown* pUnkOuter, REFIID riid, void** ppvObject) { + if (pUnkOuter != nullptr) { + return CLASS_E_NOAGGREGATION; + } + + if (!ppvObject) { + return E_INVALIDARG; + } + *ppvObject = nullptr; + + using namespace Microsoft::WRL; + ComPtr<NotificationCallback> callback = + Make<NotificationCallback, const GUID&, const path&>(notificationGuid, + installDir); + + switch (callback->QueryInterface(riid, ppvObject)) { + case S_OK: + return S_OK; + case E_NOINTERFACE: + return E_NOINTERFACE; + default: + return E_UNEXPECTED; + } +} diff --git a/toolkit/mozapps/notificationserver/NotificationFactory.h b/toolkit/mozapps/notificationserver/NotificationFactory.h new file mode 100644 index 0000000000..0936b1ffeb --- /dev/null +++ b/toolkit/mozapps/notificationserver/NotificationFactory.h @@ -0,0 +1,31 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* 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 NotificationFactory_h__ +#define NotificationFactory_h__ + +#include <filesystem> + +#include "NotificationCallback.h" + +using namespace std::filesystem; +using namespace Microsoft::WRL; + +class NotificationFactory final : public ClassFactory<> { + public: + HRESULT STDMETHODCALLTYPE CreateInstance(IUnknown* pUnkOuter, REFIID riid, + void** ppvObject) final; + + explicit NotificationFactory(const GUID& runtimeGuid, + const path& dllInstallDir) + : notificationGuid(runtimeGuid), installDir(dllInstallDir) {} + + private: + const GUID notificationGuid = {}; + const path installDir = {}; +}; + +#endif diff --git a/toolkit/mozapps/notificationserver/moz.build b/toolkit/mozapps/notificationserver/moz.build new file mode 100644 index 0000000000..16e8d50f9f --- /dev/null +++ b/toolkit/mozapps/notificationserver/moz.build @@ -0,0 +1,33 @@ +# -*- 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", "Notifications and Alerts") + +SharedLibrary("notificationserver") + +UNIFIED_SOURCES = [ + "/mfbt/Poison.cpp", # Necessary for global poison definitions. + "NotificationCallback.cpp", + "NotificationComServer.cpp", + "NotificationFactory.cpp", +] + +DEFFILE = "notificationserver.def" + +DEFINES["MOZ_APP_NAME"] = '"%s"' % CONFIG["MOZ_APP_NAME"] +DEFINES["MOZ_APP_DISPLAYNAME"] = '"%s"' % CONFIG["MOZ_APP_DISPLAYNAME"] + +DEFINES["IMPL_MFBT"] = True + +OS_LIBS += [ + "advapi32", + "kernel32", + "user32", +] + +LIBRARY_DEFINES["MOZ_NO_MOZALLOC"] = True +DisableStlWrapping() diff --git a/toolkit/mozapps/notificationserver/notificationserver.def b/toolkit/mozapps/notificationserver/notificationserver.def new file mode 100644 index 0000000000..694d5609e3 --- /dev/null +++ b/toolkit/mozapps/notificationserver/notificationserver.def @@ -0,0 +1,6 @@ +;+# 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/. + +LIBRARY notificationserver.dll +EXPORTS DllGetClassObject PRIVATE |