// 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 lpThreadAttributes, IntPtr dwStackSize, Pointer loadLibraryAddress, Pointer lpParameter, Uint32 dwCreationFlags, Pointer lpThreadId), int Function( int hProcess, Pointer lpThreadAttributes, int dwStackSize, Pointer loadLibraryAddress, Pointer lpParameter, int dwCreationFlags, Pointer lpThreadId)>('CreateRemoteThread'); const chunkSize = 1024; Future 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 startElevatedProcess({required String executable, required String args, bool window = false}) async { var shellInput = calloc(); 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(); var shellResult = ShellExecuteEx(shellInput); return shellResult == 1; } Future startProcess({required File executable, List? args, bool useTempBatch = true, bool window = false, String? name, Map? 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('NtResumeProcess'); final _NtSuspendProcess = _ntdll.lookupFunction('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 watchProcess(int pid) async { var completer = Completer(); 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 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>? _stdout; final Stream>? _stderr; _ExtendedProcess(Process delegate, bool attached) : _delegate = delegate, _stdout = attached ? delegate.stdout.asBroadcastStream() : null, _stderr = attached ? delegate.stderr.asBroadcastStream() : null; @override Future 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> get stdout { final out = _stdout; if(out == null) { throw StateError("Output is not attached"); } return out; } @override Stream> get stderr { final err = _stderr; if(err == null) { throw StateError("Output is not attached"); } return err; } }