diff --git a/liblauncher/CMakeLists.txt b/liblauncher/CMakeLists.txt index 107a8e93..d66bdbc3 100644 --- a/liblauncher/CMakeLists.txt +++ b/liblauncher/CMakeLists.txt @@ -5,7 +5,7 @@ set(CMAKE_CXX_STANDARD 17) find_package(JNI REQUIRED) -add_library(launcher SHARED main.cpp reg.cpp) +add_library(launcher SHARED main.cpp reg.cpp elevation.cpp) include_directories(../detours/include ${JNI_INCLUDE_DIRS}) if (CMAKE_GENERATOR_PLATFORM STREQUAL "x64") set_target_properties(launcher PROPERTIES OUTPUT_NAME "launcher_amd64") @@ -17,4 +17,5 @@ elseif (CMAKE_GENERATOR_PLATFORM STREQUAL "ARM64") set_target_properties(launcher PROPERTIES OUTPUT_NAME "launcher_aarch64") target_link_libraries(launcher ${CMAKE_SOURCE_DIR}/../detours/lib.ARM64/detours.lib) endif() -set_property(TARGET launcher PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded") # multi-threaded statically-linked runtime \ No newline at end of file +set_property(TARGET launcher PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded") # multi-threaded statically-linked runtime +target_compile_options(launcher PRIVATE /W4 /WX /wd4100) \ No newline at end of file diff --git a/liblauncher/elevation.cpp b/liblauncher/elevation.cpp new file mode 100644 index 00000000..b3af59fc --- /dev/null +++ b/liblauncher/elevation.cpp @@ -0,0 +1,24 @@ +#include +#include + +extern "C" JNIEXPORT jboolean JNICALL Java_net_runelite_launcher_Launcher_isProcessElevated(JNIEnv *env, jclass clazz, jlong pid) { + HANDLE process = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, (DWORD) pid); + if (process == nullptr) { + return false; + } + + BOOL ret = false; + HANDLE hToken = nullptr; + if (OpenProcessToken(process, TOKEN_QUERY, &hToken)) { + TOKEN_ELEVATION elevation; + DWORD returnLength; + if (GetTokenInformation(hToken, TokenElevation, &elevation, sizeof(elevation), &returnLength)) { + ret = elevation.TokenIsElevated; + } + CloseHandle(hToken); + } + + CloseHandle(process); + + return (jboolean) ret; +} \ No newline at end of file diff --git a/liblauncher/reg.cpp b/liblauncher/reg.cpp index 42792d4a..f79222f0 100644 --- a/liblauncher/reg.cpp +++ b/liblauncher/reg.cpp @@ -41,4 +41,35 @@ extern "C" JNIEXPORT jstring JNICALL Java_net_runelite_launcher_Launcher_regQuer } return env->NewString(reinterpret_cast(pvData), pcbData / 2 - 1); +} + +extern "C" JNIEXPORT jboolean JNICALL Java_net_runelite_launcher_Launcher_regDeleteValue(JNIEnv *env, jclass clazz, jstring keyObj, jstring subKeyObj, + jstring valueObj) { + const jchar *keyString = env->GetStringChars(keyObj, nullptr); + const jchar *subKeyString = env->GetStringChars(subKeyObj, nullptr); + const jchar *valueString = env->GetStringChars(valueObj, nullptr); + jboolean success = false; + + HKEY hKey; + if (wcscmp(reinterpret_cast(keyString), L"HKCU") == 0) { + hKey = HKEY_CURRENT_USER; + } else if (wcscmp(reinterpret_cast(keyString), L"HKLM") == 0) { + hKey = HKEY_LOCAL_MACHINE; + } else { + rlThrow(env, "invalid keyObj"); + goto out; + } + + HKEY hKeyDel = nullptr; + if (RegOpenKeyExW(hKey, reinterpret_cast(subKeyString), 0, KEY_SET_VALUE, &hKeyDel) == ERROR_SUCCESS) { + success = RegDeleteValueW(hKeyDel, reinterpret_cast(valueString)) == ERROR_SUCCESS; + RegCloseKey(hKeyDel); + } + +out: + env->ReleaseStringChars(keyObj, keyString); + env->ReleaseStringChars(subKeyObj, subKeyString); + env->ReleaseStringChars(valueObj, valueString); + + return success; } \ No newline at end of file diff --git a/src/main/java/net/runelite/launcher/JagexLauncherCompatibility.java b/src/main/java/net/runelite/launcher/JagexLauncherCompatibility.java new file mode 100644 index 00000000..7ebc131a --- /dev/null +++ b/src/main/java/net/runelite/launcher/JagexLauncherCompatibility.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2024, YvesW + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.launcher; + +import java.nio.file.Path; +import java.nio.file.Paths; +import javax.swing.SwingUtilities; +import lombok.extern.slf4j.Slf4j; +import static net.runelite.launcher.Launcher.isProcessElevated; +import static net.runelite.launcher.Launcher.nativesLoaded; +import static net.runelite.launcher.Launcher.regDeleteValue; + +@Slf4j +class JagexLauncherCompatibility +{ + // this is set to RUNASADMIN + private static final String COMPAT_KEY = "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\AppCompatFlags\\Layers"; + + static boolean check() + { + if (!nativesLoaded) + { + log.debug("Launcher natives were not loaded. Skipping Jagex launcher compatibility check."); + return false; + } + + ProcessHandle current = ProcessHandle.current(); + ProcessHandle parent = current.parent().orElse(null); + + // The only problematic configuration is for us to be running as admin & the Jagex launcher to *not* be running as admin + if (parent == null || !processIsJagexLauncher(parent) || !isProcessElevated(current.pid()) || isProcessElevated(parent.pid())) + { + return false; + } + + log.error("RuneLite is running with elevated permissions, but the Jagex launcher is not. Privileged processes " + + "can't have environment variables passed to them from unprivileged processes. This will cause you to be " + + "unable to login. Either run RuneLite as a regular user, or run the Jagex launcher as an administrator."); + + // attempt to fix this by removing the compatibility settings + String command = current.info().command().orElse(null); + boolean regEdited = false; + if (command != null) + { + regEdited |= regDeleteValue("HKLM", COMPAT_KEY, command); // all users + regEdited |= regDeleteValue("HKCU", COMPAT_KEY, command); // current user + + if (regEdited) + { + log.info("Application compatibility settings have been unset for {}", command); + } + } + + showErrorDialog(regEdited); + return true; + } + + private static boolean processIsJagexLauncher(ProcessHandle process) + { + var info = process.info(); + if (info.command().isEmpty()) + { + return false; + } + return "JagexLauncher.exe".equals(pathFilename(info.command().get())); + } + + private static String pathFilename(String path) + { + Path p = Paths.get(path); + return p.getFileName().toString(); + } + + private static void showErrorDialog(boolean patched) + { + String command = ProcessHandle.current().info().command() + .map(JagexLauncherCompatibility::pathFilename) + .orElse(Launcher.LAUNCHER_EXECUTABLE_NAME_WIN); + var sb = new StringBuilder(); + sb.append("Running RuneLite as an administrator is incompatible with the Jagex launcher."); + if (patched) + { + sb.append(" RuneLite has attempted to fix this problem by changing the compatibility settings of ").append(command).append('.'); + sb.append(" Try running RuneLite again."); + } + sb.append(" If the problem persists, either run the Jagex launcher as administrator, or change the ") + .append(command).append(" compatibility settings to not run as administrator."); + + final var message = sb.toString(); + SwingUtilities.invokeLater(() -> + new FatalErrorDialog(message) + .open()); + } +} diff --git a/src/main/java/net/runelite/launcher/Launcher.java b/src/main/java/net/runelite/launcher/Launcher.java index 2abd496b..a5bfd929 100644 --- a/src/main/java/net/runelite/launcher/Launcher.java +++ b/src/main/java/net/runelite/launcher/Launcher.java @@ -95,6 +95,7 @@ public class Launcher private static final String USER_AGENT = "RuneLite/" + LauncherProperties.getVersion(); static final String LAUNCHER_EXECUTABLE_NAME_WIN = "RuneLite.exe"; static final String LAUNCHER_EXECUTABLE_NAME_OSX = "RuneLite"; + static boolean nativesLoaded; private static HttpClient httpClient; @@ -289,6 +290,12 @@ public static void main(String[] args) } } + if (JagexLauncherCompatibility.check()) + { + // check() opens an error dialog + return; + } + SplashScreen.stage(.05, null, "Downloading bootstrap"); Bootstrap bootstrap; try @@ -943,6 +950,7 @@ private static void initDll() { System.loadLibrary("launcher_" + arch); log.debug("Loaded launcher native launcher_{}", arch); + nativesLoaded = true; } catch (Error ex) { @@ -974,4 +982,9 @@ private static void initDllBlacklist() private static native void setBlacklistedDlls(String[] dlls); static native String regQueryString(String subKey, String value); + + // Requires elevated permissions. Current valid inputs for key are: "HKCU" and "HKLM" + static native boolean regDeleteValue(String key, String subKey, String value); + + static native boolean isProcessElevated(long pid); }