summaryrefslogtreecommitdiffstats
path: root/onlineupdate/source/service
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:06:44 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:06:44 +0000
commited5640d8b587fbcfed7dd7967f3de04b37a76f26 (patch)
tree7a5f7c6c9d02226d7471cb3cc8fbbf631b415303 /onlineupdate/source/service
parentInitial commit. (diff)
downloadlibreoffice-ed5640d8b587fbcfed7dd7967f3de04b37a76f26.tar.xz
libreoffice-ed5640d8b587fbcfed7dd7967f3de04b37a76f26.zip
Adding upstream version 4:7.4.7.upstream/4%7.4.7upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'onlineupdate/source/service')
-rw-r--r--onlineupdate/source/service/certificatecheck.cxx292
-rw-r--r--onlineupdate/source/service/certificatecheck.hxx22
-rw-r--r--onlineupdate/source/service/maintenanceservice.cxx438
-rw-r--r--onlineupdate/source/service/maintenanceservice.hxx10
-rw-r--r--onlineupdate/source/service/registrycertificates.cxx181
-rw-r--r--onlineupdate/source/service/registrycertificates.hxx13
-rw-r--r--onlineupdate/source/service/resource.hxx20
-rw-r--r--onlineupdate/source/service/servicebase.cxx97
-rw-r--r--onlineupdate/source/service/servicebase.hxx21
-rw-r--r--onlineupdate/source/service/serviceinstall.cxx850
-rw-r--r--onlineupdate/source/service/serviceinstall.hxx21
-rw-r--r--onlineupdate/source/service/windowsHelper.hxx48
-rw-r--r--onlineupdate/source/service/workmonitor.cxx770
-rw-r--r--onlineupdate/source/service/workmonitor.hxx5
14 files changed, 2788 insertions, 0 deletions
diff --git a/onlineupdate/source/service/certificatecheck.cxx b/onlineupdate/source/service/certificatecheck.cxx
new file mode 100644
index 000000000..58d70162f
--- /dev/null
+++ b/onlineupdate/source/service/certificatecheck.cxx
@@ -0,0 +1,292 @@
+/* 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 <stdio.h>
+#include <stdlib.h>
+#include <windows.h>
+#include <softpub.h>
+#include <wintrust.h>
+
+#include "certificatecheck.hxx"
+#include "servicebase.hxx"
+
+#pragma comment(lib, "wintrust.lib")
+#pragma comment(lib, "crypt32.lib")
+
+static const int ENCODING = X509_ASN_ENCODING | PKCS_7_ASN_ENCODING;
+
+/**
+ * Checks to see if a file stored at filePath matches the specified info.
+ *
+ * @param filePath The PE file path to check
+ * @param infoToMatch The acceptable information to match
+ * @return ERROR_SUCCESS if successful, ERROR_NOT_FOUND if the info
+ * does not match, or the last error otherwise.
+ */
+DWORD
+CheckCertificateForPEFile(LPCWSTR filePath,
+ CertificateCheckInfo &infoToMatch)
+{
+ HCERTSTORE certStore = nullptr;
+ HCRYPTMSG cryptMsg = nullptr;
+ PCCERT_CONTEXT certContext = nullptr;
+ PCMSG_SIGNER_INFO signerInfo = nullptr;
+ DWORD lastError = ERROR_SUCCESS;
+
+ // Get the HCERTSTORE and HCRYPTMSG from the signed file.
+ DWORD encoding, contentType, formatType;
+ BOOL result = CryptQueryObject(CERT_QUERY_OBJECT_FILE,
+ filePath,
+ CERT_QUERY_CONTENT_FLAG_PKCS7_SIGNED_EMBED,
+ CERT_QUERY_CONTENT_FLAG_ALL,
+ 0, &encoding, &contentType,
+ &formatType, &certStore, &cryptMsg, nullptr);
+ if (!result)
+ {
+ lastError = GetLastError();
+ LOG_WARN(("CryptQueryObject failed. (%d)", lastError));
+ goto cleanup;
+ }
+
+ // Pass in nullptr to get the needed signer information size.
+ DWORD signerInfoSize;
+ result = CryptMsgGetParam(cryptMsg, CMSG_SIGNER_INFO_PARAM, 0,
+ nullptr, &signerInfoSize);
+ if (!result)
+ {
+ lastError = GetLastError();
+ LOG_WARN(("CryptMsgGetParam failed. (%d)", lastError));
+ goto cleanup;
+ }
+
+ // Allocate the needed size for the signer information.
+ signerInfo = (PCMSG_SIGNER_INFO)LocalAlloc(LPTR, signerInfoSize);
+ if (!signerInfo)
+ {
+ lastError = GetLastError();
+ LOG_WARN(("Unable to allocate memory for Signer Info. (%d)", lastError));
+ goto cleanup;
+ }
+
+ // Get the signer information (PCMSG_SIGNER_INFO).
+ // In particular we want the issuer and serial number.
+ result = CryptMsgGetParam(cryptMsg, CMSG_SIGNER_INFO_PARAM, 0,
+ (PVOID)signerInfo, &signerInfoSize);
+ if (!result)
+ {
+ lastError = GetLastError();
+ LOG_WARN(("CryptMsgGetParam failed. (%d)", lastError));
+ goto cleanup;
+ }
+
+ // Search for the signer certificate in the certificate store.
+ CERT_INFO certInfo;
+ certInfo.Issuer = signerInfo->Issuer;
+ certInfo.SerialNumber = signerInfo->SerialNumber;
+ certContext = CertFindCertificateInStore(certStore, ENCODING, 0,
+ CERT_FIND_SUBJECT_CERT,
+ (PVOID)&certInfo, nullptr);
+ if (!certContext)
+ {
+ lastError = GetLastError();
+ LOG_WARN(("CertFindCertificateInStore failed. (%d)", lastError));
+ goto cleanup;
+ }
+
+ if (!DoCertificateAttributesMatch(certContext, infoToMatch))
+ {
+ lastError = ERROR_NOT_FOUND;
+ LOG_WARN(("Certificate did not match issuer or name. (%d)", lastError));
+ goto cleanup;
+ }
+
+cleanup:
+ if (signerInfo)
+ {
+ LocalFree(signerInfo);
+ }
+ if (certContext)
+ {
+ CertFreeCertificateContext(certContext);
+ }
+ if (certStore)
+ {
+ CertCloseStore(certStore, 0);
+ }
+ if (cryptMsg)
+ {
+ CryptMsgClose(cryptMsg);
+ }
+ return lastError;
+}
+
+/**
+ * Checks to see if a file stored at filePath matches the specified info.
+ *
+ * @param certContext The certificate context of the file
+ * @param infoToMatch The acceptable information to match
+ * @return FALSE if the info does not match or if any error occurs in the check
+ */
+BOOL
+DoCertificateAttributesMatch(PCCERT_CONTEXT certContext,
+ CertificateCheckInfo &infoToMatch)
+{
+ DWORD dwData;
+ LPTSTR szName = nullptr;
+
+ if (infoToMatch.issuer)
+ {
+ // Pass in nullptr to get the needed size of the issuer buffer.
+ dwData = CertGetNameString(certContext,
+ CERT_NAME_SIMPLE_DISPLAY_TYPE,
+ CERT_NAME_ISSUER_FLAG, nullptr,
+ nullptr, 0);
+
+ if (!dwData)
+ {
+ LOG_WARN(("CertGetNameString failed. (%d)", GetLastError()));
+ return FALSE;
+ }
+
+ // Allocate memory for Issuer name buffer.
+ LPTSTR szName = (LPTSTR)LocalAlloc(LPTR, dwData * sizeof(WCHAR));
+ if (!szName)
+ {
+ LOG_WARN(("Unable to allocate memory for issuer name. (%d)",
+ GetLastError()));
+ return FALSE;
+ }
+
+ // Get Issuer name.
+ if (!CertGetNameString(certContext, CERT_NAME_SIMPLE_DISPLAY_TYPE,
+ CERT_NAME_ISSUER_FLAG, nullptr, szName, dwData))
+ {
+ LOG_WARN(("CertGetNameString failed. (%d)", GetLastError()));
+ LocalFree(szName);
+ return FALSE;
+ }
+
+ // If the issuer does not match, return a failure.
+ if (!infoToMatch.issuer ||
+ wcscmp(szName, infoToMatch.issuer))
+ {
+ LocalFree(szName);
+ return FALSE;
+ }
+
+ LocalFree(szName);
+ szName = nullptr;
+ }
+
+ if (infoToMatch.name)
+ {
+ // Pass in nullptr to get the needed size of the name buffer.
+ dwData = CertGetNameString(certContext, CERT_NAME_SIMPLE_DISPLAY_TYPE,
+ 0, nullptr, nullptr, 0);
+ if (!dwData)
+ {
+ LOG_WARN(("CertGetNameString failed. (%d)", GetLastError()));
+ return FALSE;
+ }
+
+ // Allocate memory for the name buffer.
+ szName = (LPTSTR)LocalAlloc(LPTR, dwData * sizeof(WCHAR));
+ if (!szName)
+ {
+ LOG_WARN(("Unable to allocate memory for subject name. (%d)",
+ GetLastError()));
+ return FALSE;
+ }
+
+ // Obtain the name.
+ if (!(CertGetNameString(certContext, CERT_NAME_SIMPLE_DISPLAY_TYPE, 0,
+ nullptr, szName, dwData)))
+ {
+ LOG_WARN(("CertGetNameString failed. (%d)", GetLastError()));
+ LocalFree(szName);
+ return FALSE;
+ }
+
+ // If the issuer does not match, return a failure.
+ if (!infoToMatch.name ||
+ wcscmp(szName, infoToMatch.name))
+ {
+ LocalFree(szName);
+ return FALSE;
+ }
+
+ // We have a match!
+ LocalFree(szName);
+ }
+
+ // If there were any errors we would have aborted by now.
+ return TRUE;
+}
+
+/**
+ * Duplicates the specified string
+ *
+ * @param inputString The string to duplicate
+ * @return The duplicated string which should be freed by the caller.
+ */
+LPWSTR
+AllocateAndCopyWideString(LPCWSTR inputString)
+{
+ LPWSTR outputString =
+ (LPWSTR)LocalAlloc(LPTR, (wcslen(inputString) + 1) * sizeof(WCHAR));
+ if (outputString)
+ {
+ lstrcpyW(outputString, inputString);
+ }
+ return outputString;
+}
+
+/**
+ * Verifies the trust of the specified file path.
+ *
+ * @param filePath The file path to check.
+ * @return ERROR_SUCCESS if successful, or the last error code otherwise.
+ */
+DWORD
+VerifyCertificateTrustForFile(LPCWSTR filePath)
+{
+ // Setup the file to check.
+ WINTRUST_FILE_INFO fileToCheck;
+ ZeroMemory(&fileToCheck, sizeof(fileToCheck));
+ fileToCheck.cbStruct = sizeof(WINTRUST_FILE_INFO);
+ fileToCheck.pcwszFilePath = filePath;
+
+ // Setup what to check, we want to check it is signed and trusted.
+ WINTRUST_DATA trustData;
+ ZeroMemory(&trustData, sizeof(trustData));
+ trustData.cbStruct = sizeof(trustData);
+ trustData.pPolicyCallbackData = nullptr;
+ trustData.pSIPClientData = nullptr;
+ trustData.dwUIChoice = WTD_UI_NONE;
+ trustData.fdwRevocationChecks = WTD_REVOKE_NONE;
+ trustData.dwUnionChoice = WTD_CHOICE_FILE;
+ trustData.dwStateAction = 0;
+ trustData.hWVTStateData = nullptr;
+ trustData.pwszURLReference = nullptr;
+ // no UI
+ trustData.dwUIContext = 0;
+ trustData.pFile = &fileToCheck;
+
+ GUID policyGUID = WINTRUST_ACTION_GENERIC_VERIFY_V2;
+ // Check if the file is signed by something that is trusted.
+ LONG ret = WinVerifyTrust(nullptr, &policyGUID, &trustData);
+ if (ERROR_SUCCESS == ret)
+ {
+ // The hash that represents the subject is trusted and there were no
+ // verification errors. No publisher nor time stamp chain errors.
+ LOG(("The file \"%ls\" is signed and the signature was verified.",
+ filePath));
+ return ERROR_SUCCESS;
+ }
+
+ DWORD lastError = GetLastError();
+ LOG_WARN(("There was an error validating trust of the certificate for file"
+ " \"%ls\". Returned: %d. (%d)", filePath, ret, lastError));
+ return ret;
+}
diff --git a/onlineupdate/source/service/certificatecheck.hxx b/onlineupdate/source/service/certificatecheck.hxx
new file mode 100644
index 000000000..1efbcb153
--- /dev/null
+++ b/onlineupdate/source/service/certificatecheck.hxx
@@ -0,0 +1,22 @@
+/* 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 _CERTIFICATECHECK_H_
+#define _CERTIFICATECHECK_H_
+
+#include <wincrypt.h>
+
+struct CertificateCheckInfo
+{
+ LPCWSTR name;
+ LPCWSTR issuer;
+};
+
+BOOL DoCertificateAttributesMatch(PCCERT_CONTEXT pCertContext,
+ CertificateCheckInfo &infoToMatch);
+DWORD VerifyCertificateTrustForFile(LPCWSTR filePath);
+DWORD CheckCertificateForPEFile(LPCWSTR filePath,
+ CertificateCheckInfo &infoToMatch);
+
+#endif
diff --git a/onlineupdate/source/service/maintenanceservice.cxx b/onlineupdate/source/service/maintenanceservice.cxx
new file mode 100644
index 000000000..da324de54
--- /dev/null
+++ b/onlineupdate/source/service/maintenanceservice.cxx
@@ -0,0 +1,438 @@
+/* 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 <windows.h>
+#include <shlwapi.h>
+#include <stdio.h>
+#include <wchar.h>
+#include <shlobj.h>
+
+#include "serviceinstall.hxx"
+#include "maintenanceservice.hxx"
+#include "servicebase.hxx"
+#include "workmonitor.hxx"
+#include "uachelper.h"
+#include "updatehelper.h"
+
+// Link w/ subsystem window so we don't get a console when executing
+// this binary through the installer.
+#pragma comment(linker, "/SUBSYSTEM:windows")
+
+SERVICE_STATUS gSvcStatus = { 0 };
+SERVICE_STATUS_HANDLE gSvcStatusHandle = nullptr;
+HANDLE gWorkDoneEvent = nullptr;
+HANDLE gThread = nullptr;
+bool gServiceControlStopping = false;
+
+// logs are pretty small, about 20 lines, so 10 seems reasonable.
+#define LOGS_TO_KEEP 10
+
+BOOL GetLogDirectoryPath(WCHAR *path);
+
+int
+wmain(int argc, WCHAR **argv)
+{
+ if (argc < 2)
+ {
+ LOG_WARN(("missing mandatory command line argument"));
+ return 1;
+ }
+ // If command-line parameter is "install", install the service
+ // or upgrade if already installed
+ // If command line parameter is "forceinstall", install the service
+ // even if it is older than what is already installed.
+ // If command-line parameter is "upgrade", upgrade the service
+ // but do not install it if it is not already installed.
+ // If command line parameter is "uninstall", uninstall the service.
+ // Otherwise, the service is probably being started by the SCM.
+ bool forceInstall = !lstrcmpi(argv[1], L"forceinstall");
+ if (!lstrcmpi(argv[1], L"install") || forceInstall)
+ {
+ WCHAR updatePath[MAX_PATH + 1];
+ if (GetLogDirectoryPath(updatePath))
+ {
+ LogInit(updatePath, L"maintenanceservice-install.log");
+ }
+
+ SvcInstallAction action = InstallSvc;
+ if (forceInstall)
+ {
+ action = ForceInstallSvc;
+ LOG(("Installing service with force specified..."));
+ }
+ else
+ {
+ LOG(("Installing service..."));
+ }
+
+ bool ret = SvcInstall(action);
+ if (!ret)
+ {
+ LOG_WARN(("Could not install service. (%d)", GetLastError()));
+ LogFinish();
+ return 1;
+ }
+
+ LOG(("The service was installed successfully"));
+ LogFinish();
+ return 0;
+ }
+
+ if (!lstrcmpi(argv[1], L"upgrade"))
+ {
+ WCHAR updatePath[MAX_PATH + 1];
+ if (GetLogDirectoryPath(updatePath))
+ {
+ LogInit(updatePath, L"maintenanceservice-install.log");
+ }
+
+ LOG(("Upgrading service if installed..."));
+ if (!SvcInstall(UpgradeSvc))
+ {
+ LOG_WARN(("Could not upgrade service. (%d)", GetLastError()));
+ LogFinish();
+ return 1;
+ }
+
+ LOG(("The service was upgraded successfully"));
+ LogFinish();
+ return 0;
+ }
+
+ if (!lstrcmpi(argv[1], L"uninstall"))
+ {
+ WCHAR updatePath[MAX_PATH + 1];
+ if (GetLogDirectoryPath(updatePath))
+ {
+ LogInit(updatePath, L"maintenanceservice-uninstall.log");
+ }
+ LOG(("Uninstalling service..."));
+ if (!SvcUninstall())
+ {
+ LOG_WARN(("Could not uninstall service. (%d)", GetLastError()));
+ LogFinish();
+ return 1;
+ }
+ LOG(("The service was uninstalled successfully"));
+ LogFinish();
+ return 0;
+ }
+
+ SERVICE_TABLE_ENTRYW DispatchTable[] =
+ {
+ { SVC_NAME, (LPSERVICE_MAIN_FUNCTIONW) SvcMain },
+ { nullptr, nullptr }
+ };
+
+ // This call returns when the service has stopped.
+ // The process should simply terminate when the call returns.
+ if (!StartServiceCtrlDispatcherW(DispatchTable))
+ {
+ LOG_WARN(("StartServiceCtrlDispatcher failed. (%d)", GetLastError()));
+ }
+
+ return 0;
+}
+
+/**
+ * Obtains the base path where logs should be stored
+ *
+ * @param path The out buffer for the backup log path of size MAX_PATH + 1
+ * @return TRUE if successful.
+ */
+BOOL
+GetLogDirectoryPath(WCHAR *path)
+{
+ HRESULT hr = SHGetFolderPathW(nullptr, CSIDL_COMMON_APPDATA, nullptr,
+ SHGFP_TYPE_CURRENT, path);
+ if (FAILED(hr))
+ {
+ return FALSE;
+ }
+
+ if (!PathAppendSafe(path, L"Mozilla"))
+ {
+ return FALSE;
+ }
+ // The directory should already be created from the installer, but
+ // just to be safe in case someone deletes.
+ CreateDirectoryW(path, nullptr);
+
+ if (!PathAppendSafe(path, L"logs"))
+ {
+ return FALSE;
+ }
+ CreateDirectoryW(path, nullptr);
+ return TRUE;
+}
+
+/**
+ * Calculated a backup path based on the log number.
+ *
+ * @param path The out buffer to store the log path of size MAX_PATH + 1
+ * @param basePath The base directory where the calculated path should go
+ * @param logNumber The log number, 0 == updater.log
+ * @return TRUE if successful.
+ */
+BOOL
+GetBackupLogPath(LPWSTR path, LPCWSTR basePath, int logNumber)
+{
+ WCHAR logName[64] = { L'\0' };
+ wcsncpy(path, basePath, sizeof(logName) / sizeof(logName[0]) - 1);
+ if (logNumber <= 0)
+ {
+ swprintf(logName, sizeof(logName) / sizeof(logName[0]),
+ L"maintenanceservice.log");
+ }
+ else
+ {
+ swprintf(logName, sizeof(logName) / sizeof(logName[0]),
+ L"maintenanceservice-%d.log", logNumber);
+ }
+ return PathAppendSafe(path, logName);
+}
+
+/**
+ * Moves the old log files out of the way before a new one is written.
+ * If you for example keep 3 logs, then this function will do:
+ * updater2.log -> updater3.log
+ * updater1.log -> updater2.log
+ * updater.log -> updater1.log
+ * Which clears room for a new updater.log in the basePath directory
+ *
+ * @param basePath The base directory path where log files are stored
+ * @param numLogsToKeep The number of logs to keep
+ */
+void
+BackupOldLogs(LPCWSTR basePath, int numLogsToKeep)
+{
+ WCHAR oldPath[MAX_PATH + 1];
+ WCHAR newPath[MAX_PATH + 1];
+ for (int i = numLogsToKeep; i >= 1; i--)
+ {
+ if (!GetBackupLogPath(oldPath, basePath, i -1))
+ {
+ continue;
+ }
+
+ if (!GetBackupLogPath(newPath, basePath, i))
+ {
+ continue;
+ }
+
+ if (!MoveFileExW(oldPath, newPath, MOVEFILE_REPLACE_EXISTING))
+ {
+ continue;
+ }
+ }
+}
+
+/**
+ * Ensures the service is shutdown once all work is complete.
+ * There is an issue on XP SP2 and below where the service can hang
+ * in a stop pending state even though the SCM is notified of a stopped
+ * state. Control *should* be returned to StartServiceCtrlDispatcher from the
+ * call to SetServiceStatus on a stopped state in the wmain thread.
+ * Sometimes this is not the case though. This thread will terminate the process
+ * if it has been 5 seconds after all work is done and the process is still not
+ * terminated. This thread is only started once a stopped state was sent to the
+ * SCM. The stop pending hang can be reproduced intermittently even if you set
+ * a stopped state directly and never set a stop pending state. It is safe to
+ * forcefully terminate the process ourselves since all work is done once we
+ * start this thread.
+*/
+DWORD WINAPI
+EnsureProcessTerminatedThread(LPVOID)
+{
+ Sleep(5000);
+ exit(0);
+}
+
+void
+StartTerminationThread()
+{
+ // If the process does not self terminate like it should, this thread
+ // will terminate the process after 5 seconds.
+ HANDLE thread = CreateThread(nullptr, 0, EnsureProcessTerminatedThread,
+ nullptr, 0, nullptr);
+ if (thread)
+ {
+ CloseHandle(thread);
+ }
+}
+
+/**
+ * Main entry point when running as a service.
+ */
+void WINAPI
+SvcMain(DWORD argc, LPWSTR *argv)
+{
+ // Setup logging, and backup the old logs
+ WCHAR updatePath[MAX_PATH + 1];
+ if (GetLogDirectoryPath(updatePath))
+ {
+ BackupOldLogs(updatePath, LOGS_TO_KEEP);
+ LogInit(updatePath, L"maintenanceservice.log");
+ }
+
+ // Disable every privilege we don't need. Processes started using
+ // CreateProcess will use the same token as this process.
+ UACHelper::DisablePrivileges(nullptr);
+
+ // Register the handler function for the service
+ gSvcStatusHandle = RegisterServiceCtrlHandlerW(SVC_NAME, SvcCtrlHandler);
+ if (!gSvcStatusHandle)
+ {
+ LOG_WARN(("RegisterServiceCtrlHandler failed. (%d)", GetLastError()));
+ ExecuteServiceCommand(argc, argv);
+ LogFinish();
+ exit(1);
+ }
+
+ // These values will be re-used later in calls involving gSvcStatus
+ gSvcStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
+ gSvcStatus.dwServiceSpecificExitCode = 0;
+
+ // Report initial status to the SCM
+ ReportSvcStatus(SERVICE_START_PENDING, NO_ERROR, 3000);
+
+ // This event will be used to tell the SvcCtrlHandler when the work is
+ // done for when a stop command is manually issued.
+ gWorkDoneEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr);
+ if (!gWorkDoneEvent)
+ {
+ ReportSvcStatus(SERVICE_STOPPED, 1, 0);
+ StartTerminationThread();
+ return;
+ }
+
+ // Initialization complete and we're about to start working on
+ // the actual command. Report the service state as running to the SCM.
+ ReportSvcStatus(SERVICE_RUNNING, NO_ERROR, 0);
+
+ // The service command was executed, stop logging and set an event
+ // to indicate the work is done in case someone is waiting on a
+ // service stop operation.
+ ExecuteServiceCommand(argc, argv);
+ LogFinish();
+
+ SetEvent(gWorkDoneEvent);
+
+ // If we aren't already in a stopping state then tell the SCM we're stopped
+ // now. If we are already in a stopping state then the SERVICE_STOPPED state
+ // will be set by the SvcCtrlHandler.
+ if (!gServiceControlStopping)
+ {
+ ReportSvcStatus(SERVICE_STOPPED, NO_ERROR, 0);
+ StartTerminationThread();
+ }
+}
+
+/**
+ * Sets the current service status and reports it to the SCM.
+ *
+ * @param currentState The current state (see SERVICE_STATUS)
+ * @param exitCode The system error code
+ * @param waitHint Estimated time for pending operation in milliseconds
+ */
+void
+ReportSvcStatus(DWORD currentState,
+ DWORD exitCode,
+ DWORD waitHint)
+{
+ static DWORD dwCheckPoint = 1;
+
+ gSvcStatus.dwCurrentState = currentState;
+ gSvcStatus.dwWin32ExitCode = exitCode;
+ gSvcStatus.dwWaitHint = waitHint;
+
+ if (SERVICE_START_PENDING == currentState ||
+ SERVICE_STOP_PENDING == currentState)
+ {
+ gSvcStatus.dwControlsAccepted = 0;
+ }
+ else
+ {
+ gSvcStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP |
+ SERVICE_ACCEPT_SHUTDOWN;
+ }
+
+ if ((SERVICE_RUNNING == currentState) ||
+ (SERVICE_STOPPED == currentState))
+ {
+ gSvcStatus.dwCheckPoint = 0;
+ }
+ else
+ {
+ gSvcStatus.dwCheckPoint = dwCheckPoint++;
+ }
+
+ // Report the status of the service to the SCM.
+ SetServiceStatus(gSvcStatusHandle, &gSvcStatus);
+}
+
+/**
+ * Since the SvcCtrlHandler should only spend at most 30 seconds before
+ * returning, this function does the service stop work for the SvcCtrlHandler.
+*/
+DWORD WINAPI
+StopServiceAndWaitForCommandThread(LPVOID)
+{
+ do
+ {
+ ReportSvcStatus(SERVICE_STOP_PENDING, NO_ERROR, 1000);
+ }
+ while (WaitForSingleObject(gWorkDoneEvent, 100) == WAIT_TIMEOUT);
+ CloseHandle(gWorkDoneEvent);
+ gWorkDoneEvent = nullptr;
+ ReportSvcStatus(SERVICE_STOPPED, NO_ERROR, 0);
+ StartTerminationThread();
+ return 0;
+}
+
+/**
+ * Called by SCM whenever a control code is sent to the service
+ * using the ControlService function.
+ */
+void WINAPI
+SvcCtrlHandler(DWORD dwCtrl)
+{
+ // After a SERVICE_CONTROL_STOP there should be no more commands sent to
+ // the SvcCtrlHandler.
+ if (gServiceControlStopping)
+ {
+ return;
+ }
+
+ // Handle the requested control code.
+ switch (dwCtrl)
+ {
+ case SERVICE_CONTROL_SHUTDOWN:
+ case SERVICE_CONTROL_STOP:
+ {
+ gServiceControlStopping = true;
+ ReportSvcStatus(SERVICE_STOP_PENDING, NO_ERROR, 1000);
+
+ // The SvcCtrlHandler thread should not spend more than 30 seconds in
+ // shutdown so we spawn a new thread for stopping the service
+ HANDLE thread = CreateThread(nullptr, 0,
+ StopServiceAndWaitForCommandThread,
+ nullptr, 0, nullptr);
+ if (thread)
+ {
+ CloseHandle(thread);
+ }
+ else
+ {
+ // Couldn't start the thread so just call the stop ourselves.
+ // If it happens to take longer than 30 seconds the caller will
+ // get an error.
+ StopServiceAndWaitForCommandThread(nullptr);
+ }
+ }
+ break;
+ default:
+ break;
+ }
+}
diff --git a/onlineupdate/source/service/maintenanceservice.hxx b/onlineupdate/source/service/maintenanceservice.hxx
new file mode 100644
index 000000000..af5db9e29
--- /dev/null
+++ b/onlineupdate/source/service/maintenanceservice.hxx
@@ -0,0 +1,10 @@
+/* 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/. */
+
+void WINAPI SvcMain(DWORD dwArgc, LPWSTR *lpszArgv);
+void SvcInit(DWORD dwArgc, LPWSTR *lpszArgv);
+void WINAPI SvcCtrlHandler(DWORD dwCtrl);
+void ReportSvcStatus(DWORD dwCurrentState,
+ DWORD dwWin32ExitCode,
+ DWORD dwWaitHint);
diff --git a/onlineupdate/source/service/registrycertificates.cxx b/onlineupdate/source/service/registrycertificates.cxx
new file mode 100644
index 000000000..ea0905cea
--- /dev/null
+++ b/onlineupdate/source/service/registrycertificates.cxx
@@ -0,0 +1,181 @@
+/* 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 <stdio.h>
+#include <stdlib.h>
+#include <windows.h>
+
+#include <memory>
+
+#include "registrycertificates.hxx"
+#include "pathhash.h"
+#include "servicebase.hxx"
+#include "updatehelper.h"
+#define MAX_KEY_LENGTH 255
+
+namespace {
+
+struct AutoRegKey
+{
+ AutoRegKey(HKEY key):
+ mKey(key)
+ {
+ }
+
+ ~AutoRegKey()
+ {
+ releaseKey(mKey);
+ }
+
+ void releaseKey(HKEY key)
+ {
+ if (key != nullptr)
+ {
+ RegCloseKey(key);
+ }
+ }
+
+ HKEY mKey;
+
+ HKEY get()
+ {
+ return mKey;
+ }
+};
+
+}
+
+/**
+ * Verifies if the file path matches any certificate stored in the registry.
+ *
+ * @param filePath The file path of the application to check if allowed.
+ * @return TRUE if the binary matches any of the allowed certificates.
+ */
+BOOL
+DoesBinaryMatchAllowedCertificates(LPCWSTR basePathForUpdate, LPCWSTR filePath)
+{
+ WCHAR maintenanceServiceKey[MAX_PATH + 1];
+ if (!CalculateRegistryPathFromFilePath(basePathForUpdate,
+ maintenanceServiceKey))
+ {
+ return FALSE;
+ }
+
+ // We use KEY_WOW64_64KEY to always force 64-bit view.
+ // The user may have both x86 and x64 applications installed
+ // which each register information. We need a consistent place
+ // to put those certificate attributes in and hence why we always
+ // force the non redirected registry under Wow6432Node.
+ // This flag is ignored on 32bit systems.
+ HKEY baseKeyRaw;
+ LONG retCode = RegOpenKeyExW(HKEY_LOCAL_MACHINE,
+ maintenanceServiceKey, 0,
+ KEY_READ | KEY_WOW64_64KEY, &baseKeyRaw);
+ if (retCode != ERROR_SUCCESS)
+ {
+ LOG_WARN(("Could not open key. (%d)", retCode));
+ // Our tests run with a different apply directory for each test.
+ // We use this registry key on our test slaves to store the
+ // allowed name/issuers.
+ retCode = RegOpenKeyExW(HKEY_LOCAL_MACHINE,
+ TEST_ONLY_FALLBACK_KEY_PATH, 0,
+ KEY_READ | KEY_WOW64_64KEY, &baseKeyRaw);
+ if (retCode != ERROR_SUCCESS)
+ {
+ LOG_WARN(("Could not open fallback key. (%d)", retCode));
+ return FALSE;
+ }
+ }
+ AutoRegKey baseKey(baseKeyRaw);
+
+ // Get the number of subkeys.
+ DWORD subkeyCount = 0;
+ retCode = RegQueryInfoKeyW(baseKey.get(), nullptr, nullptr, nullptr, &subkeyCount,
+ nullptr, nullptr, nullptr, nullptr, nullptr,
+ nullptr, nullptr);
+ if (retCode != ERROR_SUCCESS)
+ {
+ LOG_WARN(("Could not query info key. (%d)", retCode));
+ return FALSE;
+ }
+
+ // Enumerate the subkeys, each subkey represents an allowed certificate.
+ for (DWORD i = 0; i < subkeyCount; i++)
+ {
+ WCHAR subkeyBuffer[MAX_KEY_LENGTH];
+ DWORD subkeyBufferCount = MAX_KEY_LENGTH;
+ retCode = RegEnumKeyExW(baseKey.get(), i, subkeyBuffer,
+ &subkeyBufferCount, nullptr,
+ nullptr, nullptr, nullptr);
+ if (retCode != ERROR_SUCCESS)
+ {
+ LOG_WARN(("Could not enum certs. (%d)", retCode));
+ return FALSE;
+ }
+
+ // Open the subkey for the current certificate
+ HKEY subKeyRaw;
+ retCode = RegOpenKeyExW(baseKey.get(),
+ subkeyBuffer,
+ 0,
+ KEY_READ | KEY_WOW64_64KEY,
+ &subKeyRaw);
+ AutoRegKey subKey(subKeyRaw);
+ if (retCode != ERROR_SUCCESS)
+ {
+ LOG_WARN(("Could not open subkey. (%d)", retCode));
+ continue; // Try the next subkey
+ }
+
+ const int MAX_CHAR_COUNT = 256;
+ DWORD valueBufSize = MAX_CHAR_COUNT * sizeof(WCHAR);
+ WCHAR name[MAX_CHAR_COUNT] = { L'\0' };
+ WCHAR issuer[MAX_CHAR_COUNT] = { L'\0' };
+
+ // Get the name from the registry
+ retCode = RegQueryValueExW(subKey.get(), L"name", 0, nullptr,
+ (LPBYTE)name, &valueBufSize);
+ if (retCode != ERROR_SUCCESS)
+ {
+ LOG_WARN(("Could not obtain name from registry. (%d)", retCode));
+ continue; // Try the next subkey
+ }
+
+ // Get the issuer from the registry
+ valueBufSize = MAX_CHAR_COUNT * sizeof(WCHAR);
+ retCode = RegQueryValueExW(subKey.get(), L"issuer", 0, nullptr,
+ (LPBYTE)issuer, &valueBufSize);
+ if (retCode != ERROR_SUCCESS)
+ {
+ LOG_WARN(("Could not obtain issuer from registry. (%d)", retCode));
+ continue; // Try the next subkey
+ }
+
+ CertificateCheckInfo allowedCertificate =
+ {
+ name,
+ issuer,
+ };
+
+ retCode = CheckCertificateForPEFile(filePath, allowedCertificate);
+ if (retCode != ERROR_SUCCESS)
+ {
+ LOG_WARN(("Error on certificate check. (%d)", retCode));
+ continue; // Try the next subkey
+ }
+
+ retCode = VerifyCertificateTrustForFile(filePath);
+ if (retCode != ERROR_SUCCESS)
+ {
+ LOG_WARN(("Error on certificate trust check. (%d)", retCode));
+ continue; // Try the next subkey
+ }
+
+ // Raise the roof, we found a match!
+ return TRUE;
+ }
+
+ // No certificates match, :'(
+ return FALSE;
+}
diff --git a/onlineupdate/source/service/registrycertificates.hxx b/onlineupdate/source/service/registrycertificates.hxx
new file mode 100644
index 000000000..858b05d7c
--- /dev/null
+++ b/onlineupdate/source/service/registrycertificates.hxx
@@ -0,0 +1,13 @@
+/* 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 _REGISTRYCERTIFICATES_H_
+#define _REGISTRYCERTIFICATES_H_
+
+#include "certificatecheck.hxx"
+
+BOOL DoesBinaryMatchAllowedCertificates(LPCWSTR basePathForUpdate,
+ LPCWSTR filePath);
+
+#endif
diff --git a/onlineupdate/source/service/resource.hxx b/onlineupdate/source/service/resource.hxx
new file mode 100644
index 000000000..f1a1c3836
--- /dev/null
+++ b/onlineupdate/source/service/resource.hxx
@@ -0,0 +1,20 @@
+/* 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/. */
+
+//{{NO_DEPENDENCIES}}
+// Microsoft Visual C++ generated include file.
+// Used by updater.rc
+//
+#define IDI_DIALOG 1003
+
+// Next default values for new objects
+//
+#ifdef APSTUDIO_INVOKED
+#ifndef APSTUDIO_READONLY_SYMBOLS
+#define _APS_NEXT_RESOURCE_VALUE 102
+#define _APS_NEXT_COMMAND_VALUE 40001
+#define _APS_NEXT_CONTROL_VALUE 1003
+#define _APS_NEXT_SYMED_VALUE 101
+#endif
+#endif
diff --git a/onlineupdate/source/service/servicebase.cxx b/onlineupdate/source/service/servicebase.cxx
new file mode 100644
index 000000000..4fb632878
--- /dev/null
+++ b/onlineupdate/source/service/servicebase.cxx
@@ -0,0 +1,97 @@
+/* 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 "servicebase.hxx"
+#include "windowsHelper.hxx"
+
+// Shared code between applications and updater.exe
+
+/**
+ * Verifies if 2 files are byte for byte equivalent.
+ *
+ * @param file1Path The first file to verify.
+ * @param file2Path The second file to verify.
+ * @param sameContent Out parameter, TRUE if the files are equal
+ * @return TRUE If there was no error checking the files.
+ */
+BOOL
+VerifySameFiles(LPCWSTR file1Path, LPCWSTR file2Path, BOOL &sameContent)
+{
+ sameContent = FALSE;
+ AutoHandle file1(CreateFileW(file1Path, GENERIC_READ, FILE_SHARE_READ,
+ nullptr, OPEN_EXISTING, 0, nullptr));
+ if (file1 == INVALID_HANDLE_VALUE)
+ {
+ return FALSE;
+ }
+ AutoHandle file2(CreateFileW(file2Path, GENERIC_READ, FILE_SHARE_READ,
+ nullptr, OPEN_EXISTING, 0, nullptr));
+ if (file2 == INVALID_HANDLE_VALUE)
+ {
+ return FALSE;
+ }
+
+ DWORD fileSize1 = GetFileSize(file1.get(), nullptr);
+ DWORD fileSize2 = GetFileSize(file2.get(), nullptr);
+ if (INVALID_FILE_SIZE == fileSize1 || INVALID_FILE_SIZE == fileSize2)
+ {
+ return FALSE;
+ }
+
+ if (fileSize1 != fileSize2)
+ {
+ // sameContent is already set to FALSE
+ return TRUE;
+ }
+
+ char buf1[COMPARE_BLOCKSIZE];
+ char buf2[COMPARE_BLOCKSIZE];
+ DWORD numBlocks = fileSize1 / COMPARE_BLOCKSIZE;
+ DWORD leftOver = fileSize1 % COMPARE_BLOCKSIZE;
+ DWORD readAmount;
+ for (DWORD i = 0; i < numBlocks; i++)
+ {
+ if (!ReadFile(file1.get(), buf1, COMPARE_BLOCKSIZE, &readAmount, nullptr) ||
+ readAmount != COMPARE_BLOCKSIZE)
+ {
+ return FALSE;
+ }
+
+ if (!ReadFile(file2.get(), buf2, COMPARE_BLOCKSIZE, &readAmount, nullptr) ||
+ readAmount != COMPARE_BLOCKSIZE)
+ {
+ return FALSE;
+ }
+
+ if (memcmp(buf1, buf2, COMPARE_BLOCKSIZE))
+ {
+ // sameContent is already set to FALSE
+ return TRUE;
+ }
+ }
+
+ if (leftOver)
+ {
+ if (!ReadFile(file1.get(), buf1, leftOver, &readAmount, nullptr) ||
+ readAmount != leftOver)
+ {
+ return FALSE;
+ }
+
+ if (!ReadFile(file2.get(), buf2, leftOver, &readAmount, nullptr) ||
+ readAmount != leftOver)
+ {
+ return FALSE;
+ }
+
+ if (memcmp(buf1, buf2, leftOver))
+ {
+ // sameContent is already set to FALSE
+ return TRUE;
+ }
+ }
+
+ sameContent = TRUE;
+ return TRUE;
+}
diff --git a/onlineupdate/source/service/servicebase.hxx b/onlineupdate/source/service/servicebase.hxx
new file mode 100644
index 000000000..45375fbe5
--- /dev/null
+++ b/onlineupdate/source/service/servicebase.hxx
@@ -0,0 +1,21 @@
+/* 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 <windows.h>
+#include "updatelogging.h"
+
+BOOL PathAppendSafe(LPWSTR base, LPCWSTR extra);
+BOOL VerifySameFiles(LPCWSTR file1Path, LPCWSTR file2Path, BOOL& sameContent);
+
+// 32KiB for comparing files at a time seems reasonable.
+// The bigger the better for speed, but this will be used
+// on the stack so I don't want it to be too big.
+#define COMPARE_BLOCKSIZE 32768
+
+// The following string resource value is used to uniquely identify the signed
+// Mozilla application as an updater. Before the maintenance service will
+// execute the updater it must have this updater identity string in its string
+// table. No other signed Mozilla product will have this string table value.
+#define UPDATER_IDENTITY_STRING "libreoffice-updater.exe-7bab28a0-0599-4f37-9efe-f7f8b71f05e3"
+#define IDS_UPDATER_IDENTITY 1006
diff --git a/onlineupdate/source/service/serviceinstall.cxx b/onlineupdate/source/service/serviceinstall.cxx
new file mode 100644
index 000000000..7d53d25cb
--- /dev/null
+++ b/onlineupdate/source/service/serviceinstall.cxx
@@ -0,0 +1,850 @@
+/* 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 <windows.h>
+#include <aclapi.h>
+#include <stdlib.h>
+#include <shlwapi.h>
+
+// Used for DNLEN and UNLEN
+#include <lm.h>
+
+#include "serviceinstall.hxx"
+#include "servicebase.hxx"
+#include "updatehelper.h"
+#include "shellapi.h"
+#include "readstrings.h"
+#include "errors.h"
+
+#include <memory>
+
+#pragma comment(lib, "version.lib")
+
+namespace {
+
+struct AutoServiceHandle
+{
+ AutoServiceHandle(SC_HANDLE handle):
+ mHandle(handle)
+ {
+ }
+
+ ~AutoServiceHandle()
+ {
+ releaseHandle(mHandle);
+ }
+
+ void releaseHandle(SC_HANDLE handle)
+ {
+ if (handle != nullptr)
+ CloseServiceHandle(handle);
+ }
+
+ SC_HANDLE get() const
+ {
+ return mHandle;
+ }
+
+ operator bool() const
+ {
+ return mHandle != nullptr;
+ }
+
+ void reset(SC_HANDLE handle = nullptr)
+ {
+ releaseHandle(mHandle);
+ mHandle = handle;
+ }
+
+ SC_HANDLE mHandle;
+};
+
+}
+
+/**
+ * A wrapper function to read strings for the maintenance service.
+ *
+ * @param path The path of the ini file to read from
+ * @param results The maintenance service strings that were read
+ * @return OK on success
+*/
+static int
+ReadMaintenanceServiceStrings(LPCWSTR path,
+ MaintenanceServiceStringTable *results)
+{
+ // Read in the maintenance service description string if specified.
+ const unsigned int kNumStrings = 1;
+ // TODO: moggi: needs adaptation for LibreOffice
+ const char *kServiceKeys = "MozillaMaintenanceDescription\0";
+ char serviceStrings[kNumStrings][MAX_TEXT_LEN];
+ int result = ReadStrings(path, kServiceKeys,
+ kNumStrings, serviceStrings);
+ if (result != OK)
+ {
+ serviceStrings[0][0] = '\0';
+ }
+ strncpy(results->serviceDescription,
+ serviceStrings[0], MAX_TEXT_LEN - 1);
+ results->serviceDescription[MAX_TEXT_LEN - 1] = '\0';
+ return result;
+}
+
+/**
+ * Obtains the version number from the specified PE file's version information
+ * Version Format: A.B.C.D (Example 10.0.0.300)
+ *
+ * @param path The path of the file to check the version on
+ * @param A The first part of the version number
+ * @param B The second part of the version number
+ * @param C The third part of the version number
+ * @param D The fourth part of the version number
+ * @return TRUE if successful
+ */
+static BOOL
+GetVersionNumberFromPath(LPWSTR path, DWORD &A, DWORD &B,
+ DWORD &C, DWORD &D)
+{
+ DWORD fileVersionInfoSize = GetFileVersionInfoSizeW(path, 0);
+ std::unique_ptr<char[]> fileVersionInfo(new char[fileVersionInfoSize]);
+ if (!GetFileVersionInfoW(path, 0, fileVersionInfoSize,
+ fileVersionInfo.get()))
+ {
+ LOG_WARN(("Could not obtain file info of old service. (%d)",
+ GetLastError()));
+ return FALSE;
+ }
+
+ VS_FIXEDFILEINFO *fixedFileInfo =
+ reinterpret_cast<VS_FIXEDFILEINFO *>(fileVersionInfo.get());
+ UINT size;
+ if (!VerQueryValueW(fileVersionInfo.get(), L"\\",
+ reinterpret_cast<LPVOID*>(&fixedFileInfo), &size))
+ {
+ LOG_WARN(("Could not query file version info of old service. (%d)",
+ GetLastError()));
+ return FALSE;
+ }
+
+ A = HIWORD(fixedFileInfo->dwFileVersionMS);
+ B = LOWORD(fixedFileInfo->dwFileVersionMS);
+ C = HIWORD(fixedFileInfo->dwFileVersionLS);
+ D = LOWORD(fixedFileInfo->dwFileVersionLS);
+ return TRUE;
+}
+
+/**
+ * Updates the service description with what is stored in updater.ini
+ * at the same path as the currently executing module binary.
+ *
+ * @param serviceHandle A handle to an opened service with
+ * SERVICE_CHANGE_CONFIG access right
+ * @param TRUE on success.
+*/
+BOOL
+UpdateServiceDescription(SC_HANDLE serviceHandle)
+{
+ WCHAR updaterINIPath[MAX_PATH + 1];
+ if (!GetModuleFileNameW(nullptr, updaterINIPath,
+ sizeof(updaterINIPath) /
+ sizeof(updaterINIPath[0])))
+ {
+ LOG_WARN(("Could not obtain module filename when attempting to "
+ "modify service description. (%d)", GetLastError()));
+ return FALSE;
+ }
+
+ if (!PathRemoveFileSpecW(updaterINIPath))
+ {
+ LOG_WARN(("Could not remove file spec when attempting to "
+ "modify service description. (%d)", GetLastError()));
+ return FALSE;
+ }
+
+ if (!PathAppendSafe(updaterINIPath, L"updater.ini"))
+ {
+ LOG_WARN(("Could not append updater.ini filename when attempting to "
+ "modify service description. (%d)", GetLastError()));
+ return FALSE;
+ }
+
+ if (GetFileAttributesW(updaterINIPath) == INVALID_FILE_ATTRIBUTES)
+ {
+ LOG_WARN(("updater.ini file does not exist, will not modify "
+ "service description. (%d)", GetLastError()));
+ return FALSE;
+ }
+
+ MaintenanceServiceStringTable serviceStrings;
+ int rv = ReadMaintenanceServiceStrings(updaterINIPath, &serviceStrings);
+ if (rv != OK || !strlen(serviceStrings.serviceDescription))
+ {
+ LOG_WARN(("updater.ini file does not contain a maintenance "
+ "service description."));
+ return FALSE;
+ }
+
+ WCHAR serviceDescription[MAX_TEXT_LEN];
+ if (!MultiByteToWideChar(CP_UTF8, 0,
+ serviceStrings.serviceDescription, -1,
+ serviceDescription,
+ sizeof(serviceDescription) /
+ sizeof(serviceDescription[0])))
+ {
+ LOG_WARN(("Could not convert description to wide string format. (%d)",
+ GetLastError()));
+ return FALSE;
+ }
+
+ SERVICE_DESCRIPTIONW descriptionConfig;
+ descriptionConfig.lpDescription = serviceDescription;
+ if (!ChangeServiceConfig2W(serviceHandle,
+ SERVICE_CONFIG_DESCRIPTION,
+ &descriptionConfig))
+ {
+ LOG_WARN(("Could not change service config. (%d)", GetLastError()));
+ return FALSE;
+ }
+
+ LOG(("The service description was updated successfully."));
+ return TRUE;
+}
+
+/**
+ * Determines if the MozillaMaintenance service path needs to be updated
+ * and fixes it if it is wrong.
+ *
+ * @param service A handle to the service to fix.
+ * @param currentServicePath The current (possibly wrong) path that is used.
+ * @param servicePathWasWrong Out parameter set to TRUE if a fix was needed.
+ * @return TRUE if the service path is now correct.
+*/
+BOOL
+FixServicePath(SC_HANDLE service,
+ LPCWSTR currentServicePath,
+ BOOL &servicePathWasWrong)
+{
+ // When we originally upgraded the MozillaMaintenance service we
+ // would uninstall the service on each upgrade. This had an
+ // intermittent error which could cause the service to use the file
+ // maintenanceservice_tmp.exe as the install path. Only a small number
+ // of Nightly users would be affected by this, but we check for this
+ // state here and fix the user if they are affected.
+ //
+ // We also fix the path in the case of the path not being quoted.
+ size_t currentServicePathLen = wcslen(currentServicePath);
+ bool doesServiceHaveCorrectPath =
+ currentServicePathLen > 2 &&
+ !wcsstr(currentServicePath, L"maintenanceservice_tmp.exe") &&
+ currentServicePath[0] == L'\"' &&
+ currentServicePath[currentServicePathLen - 1] == L'\"';
+
+ if (doesServiceHaveCorrectPath)
+ {
+ LOG(("The MozillaMaintenance service path is correct."));
+ servicePathWasWrong = FALSE;
+ return TRUE;
+ }
+ // This is a recoverable situation so not logging as a warning
+ LOG(("The MozillaMaintenance path is NOT correct. It was: %ls",
+ currentServicePath));
+
+ servicePathWasWrong = TRUE;
+ WCHAR fixedPath[MAX_PATH + 1] = { L'\0' };
+ wcsncpy(fixedPath, currentServicePath, MAX_PATH);
+ PathUnquoteSpacesW(fixedPath);
+ if (!PathRemoveFileSpecW(fixedPath))
+ {
+ LOG_WARN(("Couldn't remove file spec. (%d)", GetLastError()));
+ return FALSE;
+ }
+ if (!PathAppendSafe(fixedPath, L"maintenanceservice.exe"))
+ {
+ LOG_WARN(("Couldn't append file spec. (%d)", GetLastError()));
+ return FALSE;
+ }
+ PathQuoteSpacesW(fixedPath);
+
+
+ if (!ChangeServiceConfigW(service, SERVICE_NO_CHANGE, SERVICE_NO_CHANGE,
+ SERVICE_NO_CHANGE, fixedPath, nullptr, nullptr,
+ nullptr, nullptr, nullptr, nullptr))
+ {
+ LOG_WARN(("Could not fix service path. (%d)", GetLastError()));
+ return FALSE;
+ }
+
+ LOG(("Fixed service path to: %ls.", fixedPath));
+ return TRUE;
+}
+
+/**
+ * Installs or upgrades the SVC_NAME service.
+ * If an existing service is already installed, we replace it with the
+ * currently running process.
+ *
+ * @param action The action to perform.
+ * @return TRUE if the service was installed/upgraded
+ */
+BOOL
+SvcInstall(SvcInstallAction action)
+{
+ // Get a handle to the local computer SCM database with full access rights.
+ AutoServiceHandle schSCManager(OpenSCManager(nullptr, nullptr,
+ SC_MANAGER_ALL_ACCESS));
+ if (!schSCManager)
+ {
+ LOG_WARN(("Could not open service manager. (%d)", GetLastError()));
+ return FALSE;
+ }
+
+ WCHAR newServiceBinaryPath[MAX_PATH + 1];
+ if (!GetModuleFileNameW(nullptr, newServiceBinaryPath,
+ sizeof(newServiceBinaryPath) /
+ sizeof(newServiceBinaryPath[0])))
+ {
+ LOG_WARN(("Could not obtain module filename when attempting to "
+ "install service. (%d)",
+ GetLastError()));
+ return FALSE;
+ }
+
+ // Check if we already have the service installed.
+ AutoServiceHandle schService(OpenServiceW(schSCManager.get(),
+ SVC_NAME,
+ SERVICE_ALL_ACCESS));
+ DWORD lastError = GetLastError();
+ if (!schService && ERROR_SERVICE_DOES_NOT_EXIST != lastError)
+ {
+ // The service exists but we couldn't open it
+ LOG_WARN(("Could not open service. (%d)", GetLastError()));
+ return FALSE;
+ }
+
+ if (schService)
+ {
+ // The service exists but it may not have the correct permissions.
+ // This could happen if the permissions were not set correctly originally
+ // or have been changed after the installation. This will reset the
+ // permissions back to allow limited user accounts.
+ if (!SetUserAccessServiceDACL(schService.get()))
+ {
+ LOG_WARN(("Could not reset security ACE on service handle. It might not be "
+ "possible to start the service. This error should never "
+ "happen. (%d)", GetLastError()));
+ }
+
+ // The service exists and we opened it
+ DWORD bytesNeeded;
+ if (!QueryServiceConfigW(schService.get(), nullptr, 0, &bytesNeeded) &&
+ GetLastError() != ERROR_INSUFFICIENT_BUFFER)
+ {
+ LOG_WARN(("Could not determine buffer size for query service config. (%d)",
+ GetLastError()));
+ return FALSE;
+ }
+
+ // Get the service config information, in particular we want the binary
+ // path of the service.
+ std::unique_ptr<char[]> serviceConfigBuffer(new char[bytesNeeded]);
+ if (!QueryServiceConfigW(schService.get(),
+ reinterpret_cast<QUERY_SERVICE_CONFIGW*>(serviceConfigBuffer.get()),
+ bytesNeeded, &bytesNeeded))
+ {
+ LOG_WARN(("Could open service but could not query service config. (%d)",
+ GetLastError()));
+ return FALSE;
+ }
+ QUERY_SERVICE_CONFIGW &serviceConfig =
+ *reinterpret_cast<QUERY_SERVICE_CONFIGW*>(serviceConfigBuffer.get());
+
+ // Check if we need to fix the service path
+ BOOL servicePathWasWrong;
+ static BOOL alreadyCheckedFixServicePath = FALSE;
+ if (!alreadyCheckedFixServicePath)
+ {
+ if (!FixServicePath(schService.get(), serviceConfig.lpBinaryPathName,
+ servicePathWasWrong))
+ {
+ LOG_WARN(("Could not fix service path. This should never happen. (%d)",
+ GetLastError()));
+ // True is returned because the service is pointing to
+ // maintenanceservice_tmp.exe so it actually was upgraded to the
+ // newest installed service.
+ return TRUE;
+ }
+ else if (servicePathWasWrong)
+ {
+ // Now that the path is fixed we should re-attempt the install.
+ // This current process' image path is maintenanceservice_tmp.exe.
+ // The service used to point to maintenanceservice_tmp.exe.
+ // The service was just fixed to point to maintenanceservice.exe.
+ // Re-attempting an install from scratch will work as normal.
+ alreadyCheckedFixServicePath = TRUE;
+ LOG(("Restarting install action: %d", action));
+ return SvcInstall(action);
+ }
+ }
+
+ // Ensure the service path is not quoted. We own this memory and know it to
+ // be large enough for the quoted path, so it is large enough for the
+ // unquoted path. This function cannot fail.
+ PathUnquoteSpacesW(serviceConfig.lpBinaryPathName);
+
+ // Obtain the existing maintenanceservice file's version number and
+ // the new file's version number. Versions are in the format of
+ // A.B.C.D.
+ DWORD existingA, existingB, existingC, existingD;
+ DWORD newA, newB, newC, newD;
+ BOOL obtainedExistingVersionInfo =
+ GetVersionNumberFromPath(serviceConfig.lpBinaryPathName,
+ existingA, existingB,
+ existingC, existingD);
+ if (!GetVersionNumberFromPath(newServiceBinaryPath, newA,
+ newB, newC, newD))
+ {
+ LOG_WARN(("Could not obtain version number from new path"));
+ return FALSE;
+ }
+
+ // Check if we need to replace the old binary with the new one
+ // If we couldn't get the old version info then we assume we should
+ // replace it.
+ if (ForceInstallSvc == action ||
+ !obtainedExistingVersionInfo ||
+ (existingA < newA) ||
+ (existingA == newA && existingB < newB) ||
+ (existingA == newA && existingB == newB &&
+ existingC < newC) ||
+ (existingA == newA && existingB == newB &&
+ existingC == newC && existingD < newD))
+ {
+
+ // We have a newer updater, so update the description from the INI file.
+ UpdateServiceDescription(schService.get());
+
+ schService.reset();
+ if (!StopService())
+ {
+ return FALSE;
+ }
+
+ if (!wcscmp(newServiceBinaryPath, serviceConfig.lpBinaryPathName))
+ {
+ LOG(("File is already in the correct location, no action needed for "
+ "upgrade. The path is: \"%ls\"", newServiceBinaryPath));
+ return TRUE;
+ }
+
+ BOOL result = TRUE;
+
+ // Attempt to copy the new binary over top the existing binary.
+ // If there is an error we try to move it out of the way and then
+ // copy it in. First try the safest / easiest way to overwrite the file.
+ if (!CopyFileW(newServiceBinaryPath,
+ serviceConfig.lpBinaryPathName, FALSE))
+ {
+ LOG_WARN(("Could not overwrite old service binary file. "
+ "This should never happen, but if it does the next "
+ "upgrade will fix it, the service is not a critical "
+ "component that needs to be installed for upgrades "
+ "to work. (%d)", GetLastError()));
+
+ // We rename the last 3 filename chars in an unsafe way. Manually
+ // verify there are more than 3 chars for safe failure in MoveFileExW.
+ const size_t len = wcslen(serviceConfig.lpBinaryPathName);
+ if (len > 3)
+ {
+ // Calculate the temp file path that we're moving the file to. This
+ // is the same as the proper service path but with a .old extension.
+ LPWSTR oldServiceBinaryTempPath =
+ new WCHAR[len + 1];
+ memset(oldServiceBinaryTempPath, 0, (len + 1) * sizeof (WCHAR));
+ wcsncpy(oldServiceBinaryTempPath, serviceConfig.lpBinaryPathName, len);
+ // Rename the last 3 chars to 'old'
+ wcsncpy(oldServiceBinaryTempPath + len - 3, L"old", 3);
+
+ // Move the current (old) service file to the temp path.
+ if (MoveFileExW(serviceConfig.lpBinaryPathName,
+ oldServiceBinaryTempPath,
+ MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH))
+ {
+ // The old binary is moved out of the way, copy in the new one.
+ if (!CopyFileW(newServiceBinaryPath,
+ serviceConfig.lpBinaryPathName, FALSE))
+ {
+ // It is best to leave the old service binary in this condition.
+ LOG_WARN(("The new service binary could not be copied in."
+ " The service will not be upgraded."));
+ result = FALSE;
+ }
+ else
+ {
+ LOG(("The new service binary was copied in by first moving the"
+ " old one out of the way."));
+ }
+
+ // Attempt to get rid of the old service temp path.
+ if (DeleteFileW(oldServiceBinaryTempPath))
+ {
+ LOG(("The old temp service path was deleted: %ls.",
+ oldServiceBinaryTempPath));
+ }
+ else
+ {
+ // The old temp path could not be removed. It will be removed
+ // the next time the user can't copy the binary in or on uninstall.
+ LOG_WARN(("The old temp service path was not deleted."));
+ }
+ }
+ else
+ {
+ // It is best to leave the old service binary in this condition.
+ LOG_WARN(("Could not move old service file out of the way from:"
+ " \"%ls\" to \"%ls\". Service will not be upgraded. (%d)",
+ serviceConfig.lpBinaryPathName,
+ oldServiceBinaryTempPath, GetLastError()));
+ result = FALSE;
+ }
+ delete[] oldServiceBinaryTempPath;
+ }
+ else
+ {
+ // It is best to leave the old service binary in this condition.
+ LOG_WARN(("Service binary path was less than 3, service will"
+ " not be updated. This should never happen."));
+ result = FALSE;
+ }
+ }
+ else
+ {
+ LOG(("The new service binary was copied in."));
+ }
+
+ // We made a copy of ourselves to the existing location.
+ // The tmp file (the process of which we are executing right now) will be
+ // left over. Attempt to delete the file on the next reboot.
+ if (MoveFileExW(newServiceBinaryPath, nullptr,
+ MOVEFILE_DELAY_UNTIL_REBOOT))
+ {
+ LOG(("Deleting the old file path on the next reboot: %ls.",
+ newServiceBinaryPath));
+ }
+ else
+ {
+ LOG_WARN(("Call to delete the old file path failed: %ls.",
+ newServiceBinaryPath));
+ }
+
+ return result;
+ }
+
+ // We don't need to copy ourselves to the existing location.
+ // The tmp file (the process of which we are executing right now) will be
+ // left over. Attempt to delete the file on the next reboot.
+ MoveFileExW(newServiceBinaryPath, nullptr, MOVEFILE_DELAY_UNTIL_REBOOT);
+
+ // nothing to do, we already have a newer service installed
+ return TRUE;
+ }
+
+ // If the service does not exist and we are upgrading, don't install it.
+ if (UpgradeSvc == action)
+ {
+ // The service does not exist and we are upgrading, so don't install it
+ return TRUE;
+ }
+
+ // Quote the path only if it contains spaces.
+ PathQuoteSpacesW(newServiceBinaryPath);
+ // The service does not already exist so create the service as on demand
+ schService.reset(CreateServiceW(schSCManager.get(), SVC_NAME, SVC_DISPLAY_NAME,
+ SERVICE_ALL_ACCESS, SERVICE_WIN32_OWN_PROCESS,
+ SERVICE_DEMAND_START, SERVICE_ERROR_NORMAL,
+ newServiceBinaryPath, nullptr, nullptr,
+ nullptr, nullptr, nullptr));
+ if (!schService)
+ {
+ LOG_WARN(("Could not create Windows service. "
+ "This error should never happen since a service install "
+ "should only be called when elevated. (%d)", GetLastError()));
+ return FALSE;
+ }
+
+ if (!SetUserAccessServiceDACL(schService.get()))
+ {
+ LOG_WARN(("Could not set security ACE on service handle, the service will not "
+ "be able to be started from unelevated processes. "
+ "This error should never happen. (%d)",
+ GetLastError()));
+ }
+
+ UpdateServiceDescription(schService.get());
+
+ return TRUE;
+}
+
+/**
+ * Stops the Maintenance service.
+ *
+ * @return TRUE if successful.
+ */
+BOOL
+StopService()
+{
+ // Get a handle to the local computer SCM database with full access rights.
+ AutoServiceHandle schSCManager(OpenSCManager(nullptr, nullptr,
+ SC_MANAGER_ALL_ACCESS));
+ if (!schSCManager)
+ {
+ LOG_WARN(("Could not open service manager. (%d)", GetLastError()));
+ return FALSE;
+ }
+
+ // Open the service
+ AutoServiceHandle schService(OpenServiceW(schSCManager.get(), SVC_NAME,
+ SERVICE_ALL_ACCESS));
+ if (!schService)
+ {
+ LOG_WARN(("Could not open service. (%d)", GetLastError()));
+ return FALSE;
+ }
+
+ LOG(("Sending stop request..."));
+ SERVICE_STATUS status;
+ SetLastError(ERROR_SUCCESS);
+ if (!ControlService(schService.get(), SERVICE_CONTROL_STOP, &status) &&
+ GetLastError() != ERROR_SERVICE_NOT_ACTIVE)
+ {
+ LOG_WARN(("Error sending stop request. (%d)", GetLastError()));
+ }
+
+ schSCManager.reset();
+ schService.reset();
+
+ LOG(("Waiting for service stop..."));
+ DWORD lastState = WaitForServiceStop(SVC_NAME, 30);
+
+ // The service can be in a stopped state but the exe still in use
+ // so make sure the process is really gone before proceeding
+ WaitForProcessExit(L"maintenanceservice.exe", 30);
+ LOG(("Done waiting for service stop, last service state: %d", lastState));
+
+ return lastState == SERVICE_STOPPED;
+}
+
+/**
+ * Uninstalls the Maintenance service.
+ *
+ * @return TRUE if successful.
+ */
+BOOL
+SvcUninstall()
+{
+ // Get a handle to the local computer SCM database with full access rights.
+ AutoServiceHandle schSCManager(OpenSCManager(nullptr, nullptr,
+ SC_MANAGER_ALL_ACCESS));
+ if (!schSCManager)
+ {
+ LOG_WARN(("Could not open service manager. (%d)", GetLastError()));
+ return FALSE;
+ }
+
+ // Open the service
+ AutoServiceHandle schService(OpenServiceW(schSCManager.get(), SVC_NAME,
+ SERVICE_ALL_ACCESS));
+ if (!schService)
+ {
+ LOG_WARN(("Could not open service. (%d)", GetLastError()));
+ return FALSE;
+ }
+
+ //Stop the service so it deletes faster and so the uninstaller
+ // can actually delete its EXE.
+ DWORD totalWaitTime = 0;
+ SERVICE_STATUS status;
+ static const int maxWaitTime = 1000 * 60; // Never wait more than a minute
+ if (ControlService(schService.get(), SERVICE_CONTROL_STOP, &status))
+ {
+ do
+ {
+ Sleep(status.dwWaitHint);
+ totalWaitTime += (status.dwWaitHint + 10);
+ if (status.dwCurrentState == SERVICE_STOPPED)
+ {
+ break;
+ }
+ else if (totalWaitTime > maxWaitTime)
+ {
+ break;
+ }
+ }
+ while (QueryServiceStatus(schService.get(), &status));
+ }
+
+ // Delete the service or mark it for deletion
+ BOOL deleted = DeleteService(schService.get());
+ if (!deleted)
+ {
+ deleted = (GetLastError() == ERROR_SERVICE_MARKED_FOR_DELETE);
+ }
+
+ return deleted;
+}
+
+/**
+ * Sets the access control list for user access for the specified service.
+ *
+ * @param hService The service to set the access control list on
+ * @return TRUE if successful
+ */
+BOOL
+SetUserAccessServiceDACL(SC_HANDLE hService)
+{
+ PACL pNewAcl = nullptr;
+ PSECURITY_DESCRIPTOR psd = nullptr;
+ DWORD lastError = SetUserAccessServiceDACL(hService, pNewAcl, psd);
+ if (pNewAcl)
+ {
+ LocalFree((HLOCAL)pNewAcl);
+ }
+ if (psd)
+ {
+ LocalFree((LPVOID)psd);
+ }
+ return ERROR_SUCCESS == lastError;
+}
+
+/**
+ * Sets the access control list for user access for the specified service.
+ *
+ * @param hService The service to set the access control list on
+ * @param pNewAcl The out param ACL which should be freed by caller
+ * @param psd out param security descriptor, should be freed by caller
+ * @return ERROR_SUCCESS if successful
+ */
+DWORD
+SetUserAccessServiceDACL(SC_HANDLE hService, PACL &pNewAcl,
+ PSECURITY_DESCRIPTOR psd)
+{
+ // Get the current security descriptor needed size
+ DWORD needed = 0;
+ if (!QueryServiceObjectSecurity(hService, DACL_SECURITY_INFORMATION,
+ &psd, 0, &needed))
+ {
+ if (GetLastError() != ERROR_INSUFFICIENT_BUFFER)
+ {
+ LOG_WARN(("Could not query service object security size. (%d)",
+ GetLastError()));
+ return GetLastError();
+ }
+
+ DWORD size = needed;
+ psd = (PSECURITY_DESCRIPTOR)LocalAlloc(LPTR, size);
+ if (!psd)
+ {
+ LOG_WARN(("Could not allocate security descriptor. (%d)",
+ GetLastError()));
+ return ERROR_INSUFFICIENT_BUFFER;
+ }
+
+ // Get the actual security descriptor now
+ if (!QueryServiceObjectSecurity(hService, DACL_SECURITY_INFORMATION,
+ psd, size, &needed))
+ {
+ LOG_WARN(("Could not allocate security descriptor. (%d)",
+ GetLastError()));
+ return GetLastError();
+ }
+ }
+
+ // Get the current DACL from the security descriptor.
+ PACL pacl = nullptr;
+ BOOL bDaclPresent = FALSE;
+ BOOL bDaclDefaulted = FALSE;
+ if ( !GetSecurityDescriptorDacl(psd, &bDaclPresent, &pacl,
+ &bDaclDefaulted))
+ {
+ LOG_WARN(("Could not obtain DACL. (%d)", GetLastError()));
+ return GetLastError();
+ }
+
+ PSID sid;
+ DWORD SIDSize = SECURITY_MAX_SID_SIZE;
+ sid = LocalAlloc(LMEM_FIXED, SIDSize);
+ if (!sid)
+ {
+ LOG_WARN(("Could not allocate SID memory. (%d)", GetLastError()));
+ return GetLastError();
+ }
+
+ if (!CreateWellKnownSid(WinBuiltinUsersSid, nullptr, sid, &SIDSize))
+ {
+ DWORD lastError = GetLastError();
+ LOG_WARN(("Could not create well known SID. (%d)", lastError));
+ LocalFree(sid);
+ return lastError;
+ }
+
+ // Lookup the account name, the function fails if you don't pass in
+ // a buffer for the domain name but it's not used since we're using
+ // the built in account Sid.
+ SID_NAME_USE accountType;
+ WCHAR accountName[UNLEN + 1] = { L'\0' };
+ WCHAR domainName[DNLEN + 1] = { L'\0' };
+ DWORD accountNameSize = UNLEN + 1;
+ DWORD domainNameSize = DNLEN + 1;
+ if (!LookupAccountSidW(nullptr, sid, accountName,
+ &accountNameSize,
+ domainName, &domainNameSize, &accountType))
+ {
+ LOG_WARN(("Could not lookup account Sid, will try Users. (%d)",
+ GetLastError()));
+ wcsncpy(accountName, L"Users", UNLEN);
+ }
+
+ // We already have the group name so we can get rid of the SID
+ FreeSid(sid);
+ sid = nullptr;
+
+ // Build the ACE, BuildExplicitAccessWithName cannot fail so it is not logged.
+ EXPLICIT_ACCESS ea;
+ BuildExplicitAccessWithNameW(&ea, accountName,
+ SERVICE_START | SERVICE_STOP | GENERIC_READ,
+ SET_ACCESS, NO_INHERITANCE);
+ DWORD lastError = SetEntriesInAclW(1, (PEXPLICIT_ACCESS)&ea, pacl, &pNewAcl);
+ if (ERROR_SUCCESS != lastError)
+ {
+ LOG_WARN(("Could not set entries in ACL. (%d)", lastError));
+ return lastError;
+ }
+
+ // Initialize a new security descriptor.
+ SECURITY_DESCRIPTOR sd;
+ if (!InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION))
+ {
+ LOG_WARN(("Could not initialize security descriptor. (%d)",
+ GetLastError()));
+ return GetLastError();
+ }
+
+ // Set the new DACL in the security descriptor.
+ if (!SetSecurityDescriptorDacl(&sd, TRUE, pNewAcl, FALSE))
+ {
+ LOG_WARN(("Could not set security descriptor DACL. (%d)",
+ GetLastError()));
+ return GetLastError();
+ }
+
+ // Set the new security descriptor for the service object.
+ if (!SetServiceObjectSecurity(hService, DACL_SECURITY_INFORMATION, &sd))
+ {
+ LOG_WARN(("Could not set object security. (%d)",
+ GetLastError()));
+ return GetLastError();
+ }
+
+ // Woohoo, raise the roof
+ LOG(("User access was set successfully on the service."));
+ return ERROR_SUCCESS;
+}
diff --git a/onlineupdate/source/service/serviceinstall.hxx b/onlineupdate/source/service/serviceinstall.hxx
new file mode 100644
index 000000000..1afaada84
--- /dev/null
+++ b/onlineupdate/source/service/serviceinstall.hxx
@@ -0,0 +1,21 @@
+/* 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 "readstrings.h"
+
+#define SVC_DISPLAY_NAME L"Mozilla Maintenance Service"
+
+enum SvcInstallAction { UpgradeSvc, InstallSvc, ForceInstallSvc };
+BOOL SvcInstall(SvcInstallAction action);
+BOOL SvcUninstall();
+BOOL StopService();
+BOOL SetUserAccessServiceDACL(SC_HANDLE hService);
+DWORD SetUserAccessServiceDACL(SC_HANDLE hService, PACL &pNewAcl,
+ PSECURITY_DESCRIPTOR psd);
+
+struct MaintenanceServiceStringTable
+{
+ char serviceDescription[MAX_TEXT_LEN];
+};
+
diff --git a/onlineupdate/source/service/windowsHelper.hxx b/onlineupdate/source/service/windowsHelper.hxx
new file mode 100644
index 000000000..e17b40c61
--- /dev/null
+++ b/onlineupdate/source/service/windowsHelper.hxx
@@ -0,0 +1,48 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * 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 INCLUDED_ONLINEUPDATE_SERVICE_WINDOWSHELPER_HXX
+#define INCLUDED_ONLINEUPDATE_SERVICE_WINDOWSHELPER_HXX
+
+struct AutoHandle
+{
+ AutoHandle(HANDLE handle):
+ mHandle(handle)
+ {
+ }
+
+ ~AutoHandle()
+ {
+ release(mHandle);
+ }
+
+ void release(HANDLE handle)
+ {
+ if (handle && handle != INVALID_HANDLE_VALUE)
+ {
+ CloseHandle(handle);
+ }
+ }
+
+ HANDLE get() const
+ {
+ return mHandle;
+ }
+
+ bool operator==(const AutoHandle& rhs) const
+ {
+ return mHandle == rhs.mHandle;
+ }
+
+ HANDLE mHandle;
+};
+
+#endif
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */
diff --git a/onlineupdate/source/service/workmonitor.cxx b/onlineupdate/source/service/workmonitor.cxx
new file mode 100644
index 000000000..6228b1e6c
--- /dev/null
+++ b/onlineupdate/source/service/workmonitor.cxx
@@ -0,0 +1,770 @@
+/* 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 <shlobj.h>
+#include <shlwapi.h>
+#include <wtsapi32.h>
+#include <userenv.h>
+#include <shellapi.h>
+#include <cstddef>
+
+#pragma comment(lib, "wtsapi32.lib")
+#pragma comment(lib, "userenv.lib")
+#pragma comment(lib, "shlwapi.lib")
+#pragma comment(lib, "ole32.lib")
+#pragma comment(lib, "rpcrt4.lib")
+
+#include "workmonitor.hxx"
+#include "serviceinstall.hxx"
+#include "servicebase.hxx"
+#include "registrycertificates.hxx"
+#include "uachelper.h"
+#include "updatehelper.h"
+#include "errors.h"
+#include "windowsHelper.hxx"
+
+// Wait 15 minutes for an update operation to run at most.
+// Updates usually take less than a minute so this seems like a
+// significantly large and safe amount of time to wait.
+static const int TIME_TO_WAIT_ON_UPDATER = 15 * 60 * 1000;
+wchar_t* MakeCommandLine(int argc, wchar_t **argv);
+BOOL WriteStatusFailure(LPCWSTR updateDirPath, int errorCode);
+BOOL PathGetSiblingFilePath(LPWSTR destinationBuffer, LPCWSTR siblingFilePath,
+ LPCWSTR newFileName);
+
+/*
+ * Read the update.status file and sets isApplying to true if
+ * the status is set to applying.
+ *
+ * @param updateDirPath The directory where update.status is stored
+ * @param isApplying Out parameter for specifying if the status
+ * is set to applying or not.
+ * @return TRUE if the information was filled.
+*/
+static BOOL
+IsStatusApplying(LPCWSTR updateDirPath, BOOL &isApplying)
+{
+ isApplying = FALSE;
+ WCHAR updateStatusFilePath[MAX_PATH + 1] = {L'\0'};
+ wcsncpy(updateStatusFilePath, updateDirPath, MAX_PATH);
+ if (!PathAppendSafe(updateStatusFilePath, L"update.status"))
+ {
+ LOG_WARN(("Could not append path for update.status file"));
+ return FALSE;
+ }
+
+ AutoHandle statusFile(CreateFileW(updateStatusFilePath, GENERIC_READ,
+ FILE_SHARE_READ |
+ FILE_SHARE_WRITE |
+ FILE_SHARE_DELETE,
+ nullptr, OPEN_EXISTING, 0, nullptr));
+
+ if (statusFile == INVALID_HANDLE_VALUE)
+ {
+ LOG_WARN(("Could not open update.status file"));
+ return FALSE;
+ }
+
+ char buf[32] = { 0 };
+ DWORD read;
+ if (!ReadFile(statusFile.get(), buf, sizeof(buf), &read, nullptr))
+ {
+ LOG_WARN(("Could not read from update.status file"));
+ return FALSE;
+ }
+
+ LOG(("updater.exe returned status: %s", buf));
+
+ const char kApplying[] = "applying";
+ isApplying = strncmp(buf, kApplying,
+ sizeof(kApplying) - 1) == 0;
+ return TRUE;
+}
+
+/**
+ * Determines whether we're staging an update.
+ *
+ * @param argc The argc value normally sent to updater.exe
+ * @param argv The argv value normally sent to updater.exe
+ * @return boolean True if we're staging an update
+ */
+static bool
+IsUpdateBeingStaged(int argc, LPWSTR *argv)
+{
+ // PID will be set to -1 if we're supposed to stage an update.
+ return argc == 4 && !wcscmp(argv[3], L"-1") ||
+ argc == 5 && !wcscmp(argv[4], L"-1");
+}
+
+/**
+ * Determines whether the param only contains digits.
+ *
+ * @param str The string to check
+ * @param boolean True if the param only contains digits
+ */
+static bool
+IsDigits(WCHAR *str)
+{
+ while (*str)
+ {
+ if (!iswdigit(*str++))
+ {
+ return FALSE;
+ }
+ }
+ return TRUE;
+}
+
+/**
+ * Determines whether the command line contains just the directory to apply the
+ * update to (old command line) or if it contains the installation directory and
+ * the directory to apply the update to.
+ *
+ * @param argc The argc value normally sent to updater.exe
+ * @param argv The argv value normally sent to updater.exe
+ * @param boolean True if the command line contains just the directory to apply
+ * the update to
+ */
+static bool
+IsOldCommandline(int argc, LPWSTR *argv)
+{
+ return argc == 4 && !wcscmp(argv[3], L"-1") ||
+ argc >= 4 && (wcsstr(argv[3], L"/replace") || IsDigits(argv[3]));
+}
+
+/**
+ * Gets the installation directory from the arguments passed to updater.exe.
+ *
+ * @param argcTmp The argc value normally sent to updater.exe
+ * @param argvTmp The argv value normally sent to updater.exe
+ * @param aResultDir Buffer to hold the installation directory.
+ */
+static BOOL
+GetInstallationDir(int argcTmp, LPWSTR *argvTmp, WCHAR aResultDir[MAX_PATH + 1])
+{
+ int index = 3;
+ if (IsOldCommandline(argcTmp, argvTmp))
+ {
+ index = 2;
+ }
+
+ if (argcTmp < index)
+ {
+ return FALSE;
+ }
+
+ wcsncpy(aResultDir, argvTmp[2], MAX_PATH);
+ WCHAR* backSlash = wcsrchr(aResultDir, L'\\');
+ // Make sure that the path does not include trailing backslashes
+ if (backSlash && (backSlash[1] == L'\0'))
+ {
+ *backSlash = L'\0';
+ }
+
+ // The new command line's argv[2] is always the installation directory.
+ if (index == 2)
+ {
+ bool backgroundUpdate = IsUpdateBeingStaged(argcTmp, argvTmp);
+ bool replaceRequest = (argcTmp >= 4 && wcsstr(argvTmp[3], L"/replace"));
+ if (backgroundUpdate || replaceRequest)
+ {
+ return PathRemoveFileSpecW(aResultDir);
+ }
+ }
+ return TRUE;
+}
+
+/**
+ * Runs an update process as the service using the SYSTEM account.
+ *
+ * @param argc The number of arguments in argv
+ * @param argv The arguments normally passed to updater.exe
+ * argv[0] must be the path to updater.exe
+ * @param processStarted Set to TRUE if the process was started.
+ * @return TRUE if the update process was run had a return code of 0.
+ */
+BOOL
+StartUpdateProcess(int argc,
+ LPWSTR *argv,
+ LPCWSTR installDir,
+ BOOL &processStarted)
+{
+ LOG(("Starting update process as the service in session 0."));
+ STARTUPINFO si = {0};
+ si.cb = sizeof(STARTUPINFO);
+ si.lpDesktop = L"winsta0\\Default";
+ PROCESS_INFORMATION pi = {0};
+
+ // The updater command line is of the form:
+ // updater.exe update-dir apply [wait-pid [callback-dir callback-path args]]
+ LPWSTR cmdLine = MakeCommandLine(argc, argv);
+
+ int index = 3;
+ if (IsOldCommandline(argc, argv))
+ {
+ index = 2;
+ }
+
+ // If we're about to start the update process from session 0,
+ // then we should not show a GUI. This only really needs to be done
+ // on Vista and higher, but it's better to keep everything consistent
+ // across all OS if it's of no harm.
+ if (argc >= index)
+ {
+ // Setting the desktop to blank will ensure no GUI is displayed
+ si.lpDesktop = L"";
+ si.dwFlags |= STARTF_USESHOWWINDOW;
+ si.wShowWindow = SW_HIDE;
+ }
+
+ // We move the updater.ini file out of the way because we will handle
+ // executing PostUpdate through the service. We handle PostUpdate from
+ // the service because there are some per user things that happen that
+ // can't run in session 0 which we run updater.exe in.
+ // Once we are done running updater.exe we rename updater.ini back so
+ // that if there were any errors the next updater.exe will run correctly.
+ WCHAR updaterINI[MAX_PATH + 1];
+ WCHAR updaterINITemp[MAX_PATH + 1];
+ BOOL selfHandlePostUpdate = FALSE;
+ // We use the updater.ini from the same directory as the updater.exe
+ // because of background updates.
+ if (PathGetSiblingFilePath(updaterINI, argv[0], L"updater.ini") &&
+ PathGetSiblingFilePath(updaterINITemp, argv[0], L"updater.tmp"))
+ {
+ selfHandlePostUpdate = MoveFileExW(updaterINI, updaterINITemp,
+ MOVEFILE_REPLACE_EXISTING);
+ }
+
+ // Add an env var for USING_SERVICE so the updater.exe can
+ // do anything special that it needs to do for service updates.
+ // Search in updater.cpp for more info on USING_SERVICE.
+ putenv(const_cast<char*>("USING_SERVICE=1"));
+ LOG(("Starting service with cmdline: %ls", cmdLine));
+ processStarted = CreateProcessW(argv[0], cmdLine,
+ nullptr, nullptr, FALSE,
+ CREATE_DEFAULT_ERROR_MODE,
+ nullptr,
+ nullptr, &si, &pi);
+ // Empty value on putenv is how you remove an env variable in Windows
+ putenv(const_cast<char*>("USING_SERVICE="));
+
+ BOOL updateWasSuccessful = FALSE;
+ if (processStarted)
+ {
+ // Wait for the updater process to finish
+ LOG(("Process was started... waiting on result."));
+ DWORD waitRes = WaitForSingleObject(pi.hProcess, TIME_TO_WAIT_ON_UPDATER);
+ if (WAIT_TIMEOUT == waitRes)
+ {
+ // We waited a long period of time for updater.exe and it never finished
+ // so kill it.
+ TerminateProcess(pi.hProcess, 1);
+ }
+ else
+ {
+ // Check the return code of updater.exe to make sure we get 0
+ DWORD returnCode;
+ if (GetExitCodeProcess(pi.hProcess, &returnCode))
+ {
+ LOG(("Process finished with return code %d.", returnCode));
+ // updater returns 0 if successful.
+ updateWasSuccessful = (returnCode == 0);
+ }
+ else
+ {
+ LOG_WARN(("Process finished but could not obtain return code."));
+ }
+ }
+ CloseHandle(pi.hProcess);
+ CloseHandle(pi.hThread);
+
+ // Check just in case updater.exe didn't change the status from
+ // applying. If this is the case we report an error.
+ BOOL isApplying = FALSE;
+ if (IsStatusApplying(argv[1], isApplying) && isApplying)
+ {
+ if (updateWasSuccessful)
+ {
+ LOG(("update.status is still applying even know update "
+ " was successful."));
+ if (!WriteStatusFailure(argv[1],
+ SERVICE_STILL_APPLYING_ON_SUCCESS))
+ {
+ LOG_WARN(("Could not write update.status still applying on"
+ " success error."));
+ }
+ // Since we still had applying we know updater.exe didn't do its
+ // job correctly.
+ updateWasSuccessful = FALSE;
+ }
+ else
+ {
+ LOG_WARN(("update.status is still applying and update was not successful."));
+ if (!WriteStatusFailure(argv[1],
+ SERVICE_STILL_APPLYING_ON_FAILURE))
+ {
+ LOG_WARN(("Could not write update.status still applying on"
+ " success error."));
+ }
+ }
+ }
+ }
+ else
+ {
+ DWORD lastError = GetLastError();
+ LOG_WARN(("Could not create process as current user, "
+ "updaterPath: %ls; cmdLine: %ls. (%d)",
+ argv[0], cmdLine, lastError));
+ }
+
+ // Now that we're done with the update, restore back the updater.ini file
+ // We use it ourselves, and also we want it back in case we had any type
+ // of error so that the normal update process can use it.
+ if (selfHandlePostUpdate)
+ {
+ MoveFileExW(updaterINITemp, updaterINI, MOVEFILE_REPLACE_EXISTING);
+
+ // Only run the PostUpdate if the update was successful
+ if (updateWasSuccessful && argc > index)
+ {
+ LPCWSTR updateInfoDir = argv[1];
+ bool stagingUpdate = IsUpdateBeingStaged(argc, argv);
+
+ // Launch the PostProcess with admin access in session 0. This is
+ // actually launching the post update process but it takes in the
+ // callback app path to figure out where to apply to.
+ // The PostUpdate process with user only access will be done inside
+ // the unelevated updater.exe after the update process is complete
+ // from the service. We don't know here which session to start
+ // the user PostUpdate process from.
+ // Note that we don't need to do this if we're just staging the
+ // update in the background, as the PostUpdate step runs when
+ // performing the replacing in that case.
+ if (!stagingUpdate)
+ {
+ LOG(("Launching post update process as the service in session 0."));
+ if (!LaunchWinPostProcess(installDir, updateInfoDir, true, nullptr))
+ {
+ LOG_WARN(("The post update process could not be launched."
+ " installDir: %ls, updateInfoDir: %ls",
+ installDir, updateInfoDir));
+ }
+ }
+ }
+ }
+
+ free(cmdLine);
+ return updateWasSuccessful;
+}
+
+/**
+ * Processes a software update command
+ *
+ * @param argc The number of arguments in argv
+ * @param argv The arguments normally passed to updater.exe
+ * argv[0] must be the path to updater.exe
+ * @return TRUE if the update was successful.
+ */
+BOOL
+ProcessSoftwareUpdateCommand(DWORD argc, LPWSTR *argv)
+{
+ BOOL result = TRUE;
+ if (argc < 3)
+ {
+ LOG_WARN(("Not enough command line parameters specified. "
+ "Updating update.status."));
+
+ // We can only update update.status if argv[1] exists. argv[1] is
+ // the directory where the update.status file exists.
+ if (argc < 2 ||
+ !WriteStatusFailure(argv[1],
+ SERVICE_NOT_ENOUGH_COMMAND_LINE_ARGS))
+ {
+ LOG_WARN(("Could not write update.status service update failure. (%d)",
+ GetLastError()));
+ }
+ return FALSE;
+ }
+
+ WCHAR installDir[MAX_PATH + 1] = {L'\0'};
+ if (!GetInstallationDir(argc, argv, installDir))
+ {
+ LOG_WARN(("Could not get the installation directory"));
+ if (!WriteStatusFailure(argv[1],
+ SERVICE_INSTALLDIR_ERROR))
+ {
+ LOG_WARN(("Could not write update.status for GetInstallationDir failure."));
+ }
+ return FALSE;
+ }
+
+ // Make sure the path to the updater to use for the update is local.
+ // We do this check to make sure that file locking is available for
+ // race condition security checks.
+ BOOL isLocal = FALSE;
+ if (!IsLocalFile(argv[0], isLocal) || !isLocal)
+ {
+ LOG_WARN(("Filesystem in path %ls is not supported (%d)",
+ argv[0], GetLastError()));
+ if (!WriteStatusFailure(argv[1],
+ SERVICE_UPDATER_NOT_FIXED_DRIVE))
+ {
+ LOG_WARN(("Could not write update.status service update failure. (%d)",
+ GetLastError()));
+ }
+ return FALSE;
+ }
+
+ AutoHandle noWriteLock(CreateFileW(argv[0], GENERIC_READ, FILE_SHARE_READ,
+ nullptr, OPEN_EXISTING, 0, nullptr));
+ if (noWriteLock == INVALID_HANDLE_VALUE)
+ {
+ LOG_WARN(("Could not set no write sharing access on file. (%d)",
+ GetLastError()));
+ if (!WriteStatusFailure(argv[1],
+ SERVICE_COULD_NOT_LOCK_UPDATER))
+ {
+ LOG_WARN(("Could not write update.status service update failure. (%d)",
+ GetLastError()));
+ }
+ return FALSE;
+ }
+
+ // Verify that the updater.exe that we are executing is the same
+ // as the one in the installation directory which we are updating.
+ // The installation dir that we are installing to is installDir.
+ WCHAR installDirUpdater[MAX_PATH + 1] = { L'\0' };
+ wcsncpy(installDirUpdater, installDir, MAX_PATH);
+ if (!PathAppendSafe(installDirUpdater, L"updater.exe"))
+ {
+ LOG_WARN(("Install directory updater could not be determined."));
+ result = FALSE;
+ }
+
+ BOOL updaterIsCorrect = FALSE;
+ if (result && !VerifySameFiles(argv[0], installDirUpdater,
+ updaterIsCorrect))
+ {
+ LOG_WARN(("Error checking if the updaters are the same.\n"
+ "Path 1: %ls\nPath 2: %ls", argv[0], installDirUpdater));
+ result = FALSE;
+ }
+
+ if (result && !updaterIsCorrect)
+ {
+ LOG_WARN(("The updaters do not match, updater will not run.\n"
+ "Path 1: %ls\nPath 2: %ls", argv[0], installDirUpdater));
+ result = FALSE;
+ }
+
+ if (result)
+ {
+ LOG(("updater.exe was compared successfully to the installation directory"
+ " updater.exe."));
+ }
+ else
+ {
+ if (!WriteStatusFailure(argv[1],
+ SERVICE_UPDATER_COMPARE_ERROR))
+ {
+ LOG_WARN(("Could not write update.status updater compare failure."));
+ }
+ return FALSE;
+ }
+
+ // Check to make sure the updater.exe module has the unique updater identity.
+ // This is a security measure to make sure that the signed executable that
+ // we will run is actually an updater.
+ HMODULE updaterModule = LoadLibraryEx(argv[0], nullptr,
+ LOAD_LIBRARY_AS_DATAFILE);
+ if (!updaterModule)
+ {
+ LOG_WARN(("updater.exe module could not be loaded. (%d)", GetLastError()));
+ result = FALSE;
+ }
+ else
+ {
+ char updaterIdentity[64];
+ if (!LoadStringA(updaterModule, IDS_UPDATER_IDENTITY,
+ updaterIdentity, sizeof(updaterIdentity)))
+ {
+ LOG_WARN(("The updater.exe application does not contain the Mozilla"
+ " updater identity."));
+ result = FALSE;
+ }
+
+ if (strcmp(updaterIdentity, UPDATER_IDENTITY_STRING))
+ {
+ LOG_WARN(("The updater.exe identity string is not valid."));
+ result = FALSE;
+ }
+ FreeLibrary(updaterModule);
+ }
+
+ if (result)
+ {
+ LOG(("The updater.exe application contains the Mozilla"
+ " updater identity."));
+ }
+ else
+ {
+ if (!WriteStatusFailure(argv[1],
+ SERVICE_UPDATER_IDENTITY_ERROR))
+ {
+ LOG_WARN(("Could not write update.status no updater identity."));
+ }
+ return TRUE;
+ }
+
+ // Check for updater.exe sign problems
+ BOOL updaterSignProblem = FALSE;
+#ifndef DISABLE_UPDATER_AUTHENTICODE_CHECK
+ updaterSignProblem = !DoesBinaryMatchAllowedCertificates(installDir,
+ argv[0]);
+#endif
+
+ // Only proceed with the update if we have no signing problems
+ if (!updaterSignProblem)
+ {
+ BOOL updateProcessWasStarted = FALSE;
+ if (StartUpdateProcess(argc, argv, installDir,
+ updateProcessWasStarted))
+ {
+ LOG(("updater.exe was launched and run successfully!"));
+ LogFlush();
+
+ // Don't attempt to update the service when the update is being staged.
+ if (!IsUpdateBeingStaged(argc, argv))
+ {
+ // We might not execute code after StartServiceUpdate because
+ // the service installer will stop the service if it is running.
+ StartServiceUpdate(installDir);
+ }
+ }
+ else
+ {
+ result = FALSE;
+ LOG_WARN(("Error running update process. Updating update.status (%d)",
+ GetLastError()));
+ LogFlush();
+
+ // If the update process was started, then updater.exe is responsible for
+ // setting the failure code. If it could not be started then we do the
+ // work. We set an error instead of directly setting status pending
+ // so that the app.update.service.errors pref can be updated when
+ // the callback app restarts.
+ if (!updateProcessWasStarted)
+ {
+ if (!WriteStatusFailure(argv[1],
+ SERVICE_UPDATER_COULD_NOT_BE_STARTED))
+ {
+ LOG_WARN(("Could not write update.status service update failure. (%d)",
+ GetLastError()));
+ }
+ }
+ }
+ }
+ else
+ {
+ result = FALSE;
+ LOG_WARN(("Could not start process due to certificate check error on "
+ "updater.exe. Updating update.status. (%d)", GetLastError()));
+
+ // When there is a certificate check error on the updater.exe application,
+ // we want to write out the error.
+ if (!WriteStatusFailure(argv[1],
+ SERVICE_UPDATER_SIGN_ERROR))
+ {
+ LOG_WARN(("Could not write pending state to update.status. (%d)",
+ GetLastError()));
+ }
+ }
+
+ return result;
+}
+
+/**
+ * Obtains the updater path alongside a subdir of the service binary.
+ * The purpose of this function is to return a path that is likely high
+ * integrity and therefore more safe to execute code from.
+ *
+ * @param serviceUpdaterPath Out parameter for the path where the updater
+ * should be copied to.
+ * @return TRUE if a file path was obtained.
+ */
+BOOL
+GetSecureUpdaterPath(WCHAR serviceUpdaterPath[MAX_PATH + 1])
+{
+ if (!GetModuleFileNameW(nullptr, serviceUpdaterPath, MAX_PATH))
+ {
+ LOG_WARN(("Could not obtain module filename when attempting to "
+ "use a secure updater path. (%d)", GetLastError()));
+ return FALSE;
+ }
+
+ if (!PathRemoveFileSpecW(serviceUpdaterPath))
+ {
+ LOG_WARN(("Couldn't remove file spec when attempting to use a secure "
+ "updater path. (%d)", GetLastError()));
+ return FALSE;
+ }
+
+ if (!PathAppendSafe(serviceUpdaterPath, L"update"))
+ {
+ LOG_WARN(("Couldn't append file spec when attempting to use a secure "
+ "updater path. (%d)", GetLastError()));
+ return FALSE;
+ }
+
+ CreateDirectoryW(serviceUpdaterPath, nullptr);
+
+ if (!PathAppendSafe(serviceUpdaterPath, L"updater.exe"))
+ {
+ LOG_WARN(("Couldn't append file spec when attempting to use a secure "
+ "updater path. (%d)", GetLastError()));
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+/**
+ * Deletes the passed in updater path and the associated updater.ini file.
+ *
+ * @param serviceUpdaterPath The path to delete.
+ * @return TRUE if a file was deleted.
+ */
+BOOL
+DeleteSecureUpdater(WCHAR serviceUpdaterPath[MAX_PATH + 1])
+{
+ BOOL result = FALSE;
+ if (serviceUpdaterPath[0])
+ {
+ result = DeleteFileW(serviceUpdaterPath);
+ if (!result && GetLastError() != ERROR_PATH_NOT_FOUND &&
+ GetLastError() != ERROR_FILE_NOT_FOUND)
+ {
+ LOG_WARN(("Could not delete service updater path: '%ls'.",
+ serviceUpdaterPath));
+ }
+
+ WCHAR updaterINIPath[MAX_PATH + 1] = { L'\0' };
+ if (PathGetSiblingFilePath(updaterINIPath, serviceUpdaterPath,
+ L"updater.ini"))
+ {
+ result = DeleteFileW(updaterINIPath);
+ if (!result && GetLastError() != ERROR_PATH_NOT_FOUND &&
+ GetLastError() != ERROR_FILE_NOT_FOUND)
+ {
+ LOG_WARN(("Could not delete service updater INI path: '%ls'.",
+ updaterINIPath));
+ }
+ }
+ }
+ return result;
+}
+
+/**
+ * Executes a service command.
+ *
+ * @param argc The number of arguments in argv
+ * @param argv The service command line arguments, argv[0] and argv[1]
+ * and automatically included by Windows. argv[2] is the
+ * service command.
+ *
+ * @return FALSE if there was an error executing the service command.
+ */
+BOOL
+ExecuteServiceCommand(int argc, LPWSTR *argv)
+{
+ if (argc < 3)
+ {
+ LOG_WARN(("Not enough command line arguments to execute a service command"));
+ return FALSE;
+ }
+
+ // The tests work by making sure the log has changed, so we put a
+ // unique ID in the log.
+ RPC_WSTR guidString = RPC_WSTR(L"");
+ GUID guid;
+ HRESULT hr = CoCreateGuid(&guid);
+ if (SUCCEEDED(hr))
+ {
+ UuidToString(&guid, &guidString);
+ }
+ LOG(("Executing service command %ls, ID: %ls",
+ argv[2], reinterpret_cast<LPCWSTR>(guidString)));
+ RpcStringFree(&guidString);
+
+ BOOL result = FALSE;
+ if (!lstrcmpi(argv[2], L"software-update"))
+ {
+
+ // Use the passed in command line arguments for the update, except for the
+ // path to updater.exe. We copy updater.exe to the directory of the
+ // MozillaMaintenance service so that a low integrity process cannot
+ // replace the updater.exe at any point and use that for the update.
+ // It also makes DLL injection attacks harder.
+ LPWSTR oldUpdaterPath = argv[3];
+ WCHAR secureUpdaterPath[MAX_PATH + 1] = { L'\0' };
+ result = GetSecureUpdaterPath(secureUpdaterPath); // Does its own logging
+ if (result)
+ {
+ LOG(("Passed in path: '%ls'; Using this path for updating: '%ls'.",
+ oldUpdaterPath, secureUpdaterPath));
+ DeleteSecureUpdater(secureUpdaterPath);
+ result = CopyFileW(oldUpdaterPath, secureUpdaterPath, FALSE);
+ }
+
+ if (!result)
+ {
+ LOG_WARN(("Could not copy path to secure location. (%d)",
+ GetLastError()));
+ if (argc > 4 && !WriteStatusFailure(argv[4],
+ SERVICE_COULD_NOT_COPY_UPDATER))
+ {
+ LOG_WARN(("Could not write update.status could not copy updater error"));
+ }
+ }
+ else
+ {
+
+ // We obtained the path and copied it successfully, update the path to
+ // use for the service update.
+ argv[3] = secureUpdaterPath;
+
+ WCHAR oldUpdaterINIPath[MAX_PATH + 1] = { L'\0' };
+ WCHAR secureUpdaterINIPath[MAX_PATH + 1] = { L'\0' };
+ if (PathGetSiblingFilePath(secureUpdaterINIPath, secureUpdaterPath,
+ L"updater.ini") &&
+ PathGetSiblingFilePath(oldUpdaterINIPath, oldUpdaterPath,
+ L"updater.ini"))
+ {
+ // This is non fatal if it fails there is no real harm
+ if (!CopyFileW(oldUpdaterINIPath, secureUpdaterINIPath, FALSE))
+ {
+ LOG_WARN(("Could not copy updater.ini from: '%ls' to '%ls'. (%d)",
+ oldUpdaterINIPath, secureUpdaterINIPath, GetLastError()));
+ }
+ }
+
+ result = ProcessSoftwareUpdateCommand(argc - 3, argv + 3);
+ DeleteSecureUpdater(secureUpdaterPath);
+ }
+
+ // We might not reach here if the service install succeeded
+ // because the service self updates itself and the service
+ // installer will stop the service.
+ LOG(("Service command %ls complete.", argv[2]));
+ }
+ else
+ {
+ LOG_WARN(("Service command not recognized: %ls.", argv[2]));
+ // result is already set to FALSE
+ }
+
+ LOG(("service command %ls complete with result: %ls.",
+ argv[1], (result ? L"Success" : L"Failure")));
+ return TRUE;
+}
diff --git a/onlineupdate/source/service/workmonitor.hxx b/onlineupdate/source/service/workmonitor.hxx
new file mode 100644
index 000000000..8fc2e51a7
--- /dev/null
+++ b/onlineupdate/source/service/workmonitor.hxx
@@ -0,0 +1,5 @@
+/* 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/. */
+
+BOOL ExecuteServiceCommand(int argc, LPWSTR* argv);