mirror of
https://github.com/Auties00/Reboot-Launcher.git
synced 2026-01-13 03:02:22 +01:00
332 lines
9.5 KiB
Dart
332 lines
9.5 KiB
Dart
// ignore_for_file: non_constant_identifier_names
|
|
|
|
import 'dart:async';
|
|
import 'dart:ffi';
|
|
import 'dart:io';
|
|
import 'dart:isolate';
|
|
import 'dart:math';
|
|
|
|
import 'package:ffi/ffi.dart';
|
|
import 'package:path/path.dart' as path;
|
|
import 'package:reboot_common/common.dart';
|
|
import 'package:sync/semaphore.dart';
|
|
import 'package:win32/win32.dart';
|
|
|
|
final _ntdll = DynamicLibrary.open('ntdll.dll');
|
|
final _kernel32 = DynamicLibrary.open('kernel32.dll');
|
|
final _CreateRemoteThread = _kernel32.lookupFunction<
|
|
IntPtr Function(
|
|
IntPtr hProcess,
|
|
Pointer<SECURITY_ATTRIBUTES> lpThreadAttributes,
|
|
IntPtr dwStackSize,
|
|
Pointer loadLibraryAddress,
|
|
Pointer lpParameter,
|
|
Uint32 dwCreationFlags,
|
|
Pointer<Uint32> lpThreadId),
|
|
int Function(
|
|
int hProcess,
|
|
Pointer<SECURITY_ATTRIBUTES> lpThreadAttributes,
|
|
int dwStackSize,
|
|
Pointer loadLibraryAddress,
|
|
Pointer lpParameter,
|
|
int dwCreationFlags,
|
|
Pointer<Uint32> lpThreadId)>('CreateRemoteThread');
|
|
const chunkSize = 1024;
|
|
|
|
Future<void> injectDll(int pid, File dll) async {
|
|
// Get the path to the file
|
|
final dllPath = dll.path;
|
|
|
|
final process = OpenProcess(
|
|
0x43A,
|
|
0,
|
|
pid
|
|
);
|
|
|
|
final processAddress = GetProcAddress(
|
|
GetModuleHandle("KERNEL32".toNativeUtf16()),
|
|
"LoadLibraryA".toNativeUtf8()
|
|
);
|
|
|
|
if (processAddress == nullptr) {
|
|
throw Exception("Cannot get process address for pid $pid");
|
|
}
|
|
|
|
final dllAddress = VirtualAllocEx(
|
|
process,
|
|
nullptr,
|
|
dllPath.length + 1,
|
|
0x3000,
|
|
0x4
|
|
);
|
|
|
|
final writeMemoryResult = WriteProcessMemory(
|
|
process,
|
|
dllAddress,
|
|
dllPath.toNativeUtf8(),
|
|
dllPath.length,
|
|
nullptr
|
|
);
|
|
|
|
if (writeMemoryResult != 1) {
|
|
throw Exception("Memory write failed");
|
|
}
|
|
|
|
final createThreadResult = _CreateRemoteThread(
|
|
process,
|
|
nullptr,
|
|
0,
|
|
processAddress,
|
|
dllAddress,
|
|
0,
|
|
nullptr
|
|
);
|
|
|
|
if (createThreadResult == -1) {
|
|
throw Exception("Thread creation failed");
|
|
}
|
|
|
|
final closeResult = CloseHandle(process);
|
|
if(closeResult != 1){
|
|
throw Exception("Cannot close handle");
|
|
}
|
|
}
|
|
|
|
Future<bool> startElevatedProcess({required String executable, required String args, bool window = false}) async {
|
|
var shellInput = calloc<SHELLEXECUTEINFO>();
|
|
shellInput.ref.lpFile = executable.toNativeUtf16();
|
|
shellInput.ref.lpParameters = args.toNativeUtf16();
|
|
shellInput.ref.nShow = window ? SW_SHOWNORMAL : SW_HIDE;
|
|
shellInput.ref.fMask = ES_AWAYMODE_REQUIRED;
|
|
shellInput.ref.lpVerb = "runas".toNativeUtf16();
|
|
shellInput.ref.cbSize = sizeOf<SHELLEXECUTEINFO>();
|
|
var shellResult = ShellExecuteEx(shellInput);
|
|
return shellResult == 1;
|
|
}
|
|
|
|
Future<Process> startProcess({required File executable, List<String>? args, bool useTempBatch = true, bool window = false, String? name, Map<String, String>? environment}) async {
|
|
final argsOrEmpty = args ?? [];
|
|
if(useTempBatch) {
|
|
final tempScriptDirectory = await tempDirectory.createTemp("reboot_launcher_process");
|
|
final tempScriptFile = File("${tempScriptDirectory.path}/process.bat");
|
|
final command = window ? 'cmd.exe /k ""${executable.path}" ${argsOrEmpty.join(" ")}"' : '"${executable.path}" ${argsOrEmpty.join(" ")}';
|
|
await tempScriptFile.writeAsString(command, flush: true);
|
|
final process = await Process.start(
|
|
tempScriptFile.path,
|
|
[],
|
|
workingDirectory: executable.parent.path,
|
|
environment: environment,
|
|
mode: window ? ProcessStartMode.detachedWithStdio : ProcessStartMode.normal,
|
|
runInShell: window
|
|
);
|
|
return _withLogger(name, executable, process, window);
|
|
}
|
|
|
|
final process = await Process.start(
|
|
executable.path,
|
|
args ?? [],
|
|
workingDirectory: executable.parent.path,
|
|
mode: window ? ProcessStartMode.detachedWithStdio : ProcessStartMode.normal,
|
|
runInShell: window
|
|
);
|
|
return _withLogger(name, executable, process, window);
|
|
}
|
|
|
|
_ExtendedProcess _withLogger(String? name, File executable, Process process, bool window) {
|
|
final extendedProcess = _ExtendedProcess(process, true);
|
|
final loggingFile = File("${logsDirectory.path}\\${name ?? path.basenameWithoutExtension(executable.path)}-${DateTime.now().millisecondsSinceEpoch}.log");
|
|
loggingFile.parent.createSync(recursive: true);
|
|
if(loggingFile.existsSync()) {
|
|
loggingFile.deleteSync();
|
|
}
|
|
|
|
final semaphore = Semaphore(1);
|
|
void logEvent(String event) async {
|
|
await semaphore.acquire();
|
|
await loggingFile.writeAsString("$event\n", mode: FileMode.append, flush: true);
|
|
semaphore.release();
|
|
}
|
|
extendedProcess.stdOutput.listen(logEvent);
|
|
extendedProcess.stdError.listen(logEvent);
|
|
if(!window) {
|
|
extendedProcess.exitCode.then((value) => logEvent("Process terminated with exit code: $value\n"));
|
|
}
|
|
return extendedProcess;
|
|
}
|
|
|
|
final _NtResumeProcess = _ntdll.lookupFunction<Int32 Function(IntPtr hWnd),
|
|
int Function(int hWnd)>('NtResumeProcess');
|
|
|
|
final _NtSuspendProcess = _ntdll.lookupFunction<Int32 Function(IntPtr hWnd),
|
|
int Function(int hWnd)>('NtSuspendProcess');
|
|
|
|
bool suspend(int pid) {
|
|
final processHandle = OpenProcess(PROCESS_SUSPEND_RESUME, FALSE, pid);
|
|
final result = _NtSuspendProcess(processHandle);
|
|
CloseHandle(processHandle);
|
|
return result == 0;
|
|
}
|
|
|
|
bool resume(int pid) {
|
|
final processHandle = OpenProcess(PROCESS_SUSPEND_RESUME, FALSE, pid);
|
|
final result = _NtResumeProcess(processHandle);
|
|
CloseHandle(processHandle);
|
|
return result == 0;
|
|
}
|
|
|
|
void _watchProcess(int pid) {
|
|
final processHandle = OpenProcess(SYNCHRONIZE, FALSE, pid);
|
|
try {
|
|
WaitForSingleObject(processHandle, INFINITE);
|
|
}finally {
|
|
CloseHandle(processHandle);
|
|
}
|
|
}
|
|
|
|
Future<bool> watchProcess(int pid) async {
|
|
var completer = Completer<bool>();
|
|
var exitPort = ReceivePort();
|
|
exitPort.listen((_) {
|
|
if(!completer.isCompleted) {
|
|
completer.complete(true);
|
|
}
|
|
});
|
|
var errorPort = ReceivePort();
|
|
errorPort.listen((_) => completer.complete(false));
|
|
await Isolate.spawn(
|
|
_watchProcess,
|
|
pid,
|
|
onExit: exitPort.sendPort,
|
|
onError: errorPort.sendPort,
|
|
errorsAreFatal: true
|
|
);
|
|
return await completer.future;
|
|
}
|
|
|
|
// TODO: Template
|
|
List<String> createRebootArgs(String username, String password, bool host, GameServerType hostType, bool log, String additionalArgs) {
|
|
if(password.isEmpty) {
|
|
username = '${_parseUsername(username, host)}@projectreboot.dev';
|
|
}
|
|
|
|
password = password.isNotEmpty ? password : "Rebooted";
|
|
final args = [
|
|
"-epicapp=Fortnite",
|
|
"-epicenv=Prod",
|
|
"-epiclocale=en-us",
|
|
"-epicportal",
|
|
"-skippatchcheck",
|
|
"-nobe",
|
|
"-fromfl=eac",
|
|
"-fltoken=3db3ba5dcbd2e16703f3978d",
|
|
"-caldera=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50X2lkIjoiYmU5ZGE1YzJmYmVhNDQwN2IyZjQwZWJhYWQ4NTlhZDQiLCJnZW5lcmF0ZWQiOjE2Mzg3MTcyNzgsImNhbGRlcmFHdWlkIjoiMzgxMGI4NjMtMmE2NS00NDU3LTliNTgtNGRhYjNiNDgyYTg2IiwiYWNQcm92aWRlciI6IkVhc3lBbnRpQ2hlYXQiLCJub3RlcyI6IiIsImZhbGxiYWNrIjpmYWxzZX0.VAWQB67RTxhiWOxx7DBjnzDnXyyEnX7OljJm-j2d88G_WgwQ9wrE6lwMEHZHjBd1ISJdUO1UVUqkfLdU5nofBQ",
|
|
"-AUTH_LOGIN=$username",
|
|
"-AUTH_PASSWORD=${password.isNotEmpty ? password : "Rebooted"}",
|
|
"-AUTH_TYPE=epic"
|
|
];
|
|
|
|
if(log) {
|
|
args.add("-log");
|
|
}
|
|
|
|
if(host) {
|
|
args.addAll([
|
|
"-nosplash",
|
|
"-nosound"
|
|
]);
|
|
if(hostType == GameServerType.headless){
|
|
args.add("-nullrhi");
|
|
}
|
|
}
|
|
|
|
if(additionalArgs.isNotEmpty){
|
|
args.addAll(additionalArgs.split(" "));
|
|
}
|
|
|
|
return args;
|
|
}
|
|
|
|
void handleGameOutput({
|
|
required String line,
|
|
required bool host,
|
|
required void Function() onDisplayAttached,
|
|
required void Function() onLoggedIn,
|
|
required void Function() onMatchEnd,
|
|
required void Function() onShutdown,
|
|
required void Function() onTokenError,
|
|
required void Function() onBuildCorrupted,
|
|
}) {
|
|
if (line.contains(kShutdownLine)) {
|
|
onShutdown();
|
|
}else if(kCorruptedBuildErrors.any((element) => line.contains(element))){
|
|
onBuildCorrupted();
|
|
}else if(kCannotConnectErrors.any((element) => line.contains(element))){
|
|
onTokenError();
|
|
}else if(kLoggedInLines.every((entry) => line.contains(entry))) {
|
|
onLoggedIn();
|
|
}else if(line.contains(kGameFinishedLine) && host) {
|
|
onMatchEnd();
|
|
}else if(line.contains(kDisplayInitializedLine) && host) {
|
|
onDisplayAttached();
|
|
}
|
|
}
|
|
|
|
String _parseUsername(String username, bool host) {
|
|
if(host) {
|
|
return "Player${Random().nextInt(1000)}";
|
|
}
|
|
|
|
if (username.isEmpty) {
|
|
return kDefaultPlayerName;
|
|
}
|
|
|
|
username = username.replaceAll(RegExp("[^A-Za-z0-9]"), "").trim();
|
|
if(username.isEmpty){
|
|
return kDefaultPlayerName;
|
|
}
|
|
|
|
return username;
|
|
}
|
|
|
|
final class _ExtendedProcess implements Process {
|
|
final Process _delegate;
|
|
final Stream<List<int>>? _stdout;
|
|
final Stream<List<int>>? _stderr;
|
|
_ExtendedProcess(Process delegate, bool attached) :
|
|
_delegate = delegate,
|
|
_stdout = attached ? delegate.stdout.asBroadcastStream() : null,
|
|
_stderr = attached ? delegate.stderr.asBroadcastStream() : null;
|
|
|
|
|
|
@override
|
|
Future<int> get exitCode => _delegate.exitCode;
|
|
|
|
@override
|
|
bool kill([ProcessSignal signal = ProcessSignal.sigterm]) => _delegate.kill(signal);
|
|
|
|
@override
|
|
int get pid => _delegate.pid;
|
|
|
|
@override
|
|
IOSink get stdin => _delegate.stdin;
|
|
|
|
@override
|
|
Stream<List<int>> get stdout {
|
|
final out = _stdout;
|
|
if(out == null) {
|
|
throw StateError("Output is not attached");
|
|
}
|
|
|
|
return out;
|
|
}
|
|
|
|
@override
|
|
Stream<List<int>> get stderr {
|
|
final err = _stderr;
|
|
if(err == null) {
|
|
throw StateError("Output is not attached");
|
|
}
|
|
|
|
return err;
|
|
}
|
|
} |