This commit is contained in:
Alessandro Autiero
2024-05-22 16:49:03 +02:00
parent 9f5590d41c
commit d478650e9b
20 changed files with 383 additions and 229 deletions

View File

@@ -57,9 +57,24 @@ void main(List<String> args) async {
throw Exception("Missing game executable at: ${version.location.path}"); throw Exception("Missing game executable at: ${version.location.path}");
} }
final serverHost = result["server-host"]?.trim();
if(serverHost?.isEmpty == true){
throw Exception("Missing host name");
}
final serverPort = result["server-port"]?.trim();
if(serverPort?.isEmpty == true){
throw Exception("Missing port");
}
final serverPortNumber = serverPort == null ? null : int.tryParse(serverPort);
if(serverPort != null && serverPortNumber == null){
throw Exception("Invalid port, use only numbers");
}
var started = await startServerCli( var started = await startServerCli(
result["server-host"], serverHost,
result["server-port"], serverPortNumber,
ServerType.values.firstWhere((element) => element.name == result["server-type"]) ServerType.values.firstWhere((element) => element.name == result["server-type"])
); );
if(!started){ if(!started){

View File

@@ -30,11 +30,11 @@ Future<void> startGame() async {
Future<void> _startLauncherProcess(FortniteVersion dummyVersion) async { Future<void> _startLauncherProcess(FortniteVersion dummyVersion) async {
if (dummyVersion.launcher == null) { if (dummyVersion.launcherExecutable == null) {
return; return;
} }
_launcherProcess = await Process.start(dummyVersion.launcher!.path, []); _launcherProcess = await Process.start(dummyVersion.launcherExecutable!.path, []);
suspend(_launcherProcess!.pid); suspend(_launcherProcess!.pid);
} }

View File

@@ -3,11 +3,11 @@ import 'dart:io';
import 'package:reboot_common/common.dart'; import 'package:reboot_common/common.dart';
import 'package:reboot_common/src/util/authenticator.dart' as server; import 'package:reboot_common/src/util/authenticator.dart' as server;
Future<bool> startServerCli(String? host, String? port, ServerType type) async { Future<bool> startServerCli(String? host, int? port, ServerType type) async {
stdout.writeln("Starting backend server..."); stdout.writeln("Starting backend server...");
switch(type){ switch(type){
case ServerType.local: case ServerType.local:
var result = await pingAuthenticator(host ?? kDefaultAuthenticatorHost, port ?? kDefaultAuthenticatorPort.toString()); var result = await pingAuthenticator(host ?? kDefaultAuthenticatorHost, port ?? kDefaultAuthenticatorPort);
if(result == null){ if(result == null){
throw Exception("Local backend server is not running"); throw Exception("Local backend server is not running");
} }
@@ -17,7 +17,7 @@ Future<bool> startServerCli(String? host, String? port, ServerType type) async {
case ServerType.embedded: case ServerType.embedded:
stdout.writeln("Starting an embedded server..."); stdout.writeln("Starting an embedded server...");
await server.startEmbeddedAuthenticator(false); await server.startEmbeddedAuthenticator(false);
var result = await pingAuthenticator(host ?? kDefaultAuthenticatorHost, port ?? kDefaultAuthenticatorPort.toString()); var result = await pingAuthenticator(host ?? kDefaultAuthenticatorHost, port ?? kDefaultAuthenticatorPort);
if(result == null){ if(result == null){
throw Exception("Cannot start embedded server"); throw Exception("Cannot start embedded server");
} }
@@ -37,21 +37,7 @@ Future<bool> startServerCli(String? host, String? port, ServerType type) async {
} }
} }
Future<HttpServer?> _changeReverseProxyState(String host, String port) async { Future<HttpServer?> _changeReverseProxyState(String host, int port) async {
host = host.trim();
if(host.isEmpty){
throw Exception("Missing host name");
}
port = port.trim();
if(port.isEmpty){
throw Exception("Missing port");
}
if(int.tryParse(port) == null){
throw Exception("Invalid port, use only numbers");
}
try{ try{
var uri = await pingAuthenticator(host, port); var uri = await pingAuthenticator(host, port);
if(uri == null){ if(uri == null){

View File

@@ -9,6 +9,7 @@ export 'package:reboot_common/src/model/server_result.dart';
export 'package:reboot_common/src/model/server_type.dart'; export 'package:reboot_common/src/model/server_type.dart';
export 'package:reboot_common/src/model/update_status.dart'; export 'package:reboot_common/src/model/update_status.dart';
export 'package:reboot_common/src/model/update_timer.dart'; export 'package:reboot_common/src/model/update_timer.dart';
export 'package:reboot_common/src/model/process.dart';
export 'package:reboot_common/src/util/authenticator.dart'; export 'package:reboot_common/src/util/authenticator.dart';
export 'package:reboot_common/src/util/build.dart'; export 'package:reboot_common/src/util/build.dart';
export 'package:reboot_common/src/util/dll.dart'; export 'package:reboot_common/src/util/dll.dart';

View File

@@ -0,0 +1,23 @@
class Win32Process {
final int pid;
final Stream<String> stdOutput;
final Stream<String> errorOutput;
Win32Process({
required this.pid,
required this.stdOutput,
required this.errorOutput
});
}
class PrimitiveWin32Process {
final int pid;
final int stdOutputHandle;
final int errorOutputHandle;
PrimitiveWin32Process({
required this.pid,
required this.stdOutputHandle,
required this.errorOutputHandle
});
}

View File

@@ -7,17 +7,15 @@ import 'package:shelf_proxy/shelf_proxy.dart';
final authenticatorDirectory = Directory("${assetsDirectory.path}\\authenticator"); final authenticatorDirectory = Directory("${assetsDirectory.path}\\authenticator");
final authenticatorStartExecutable = File("${authenticatorDirectory.path}\\lawinserver.exe"); final authenticatorStartExecutable = File("${authenticatorDirectory.path}\\lawinserver.exe");
Future<int> startEmbeddedAuthenticator(bool detached) async => startBackgroundProcess( Future<Win32Process> startEmbeddedAuthenticator(bool detached) async => startProcess(
executable: authenticatorStartExecutable, executable: authenticatorStartExecutable,
window: detached window: detached,
); );
Future<HttpServer> startRemoteAuthenticatorProxy(Uri uri) async { Future<HttpServer> startRemoteAuthenticatorProxy(Uri uri) async => await serve(proxyHandler(uri), kDefaultAuthenticatorHost, kDefaultAuthenticatorPort);
print("CALLED: $uri");
return await serve(proxyHandler(uri), kDefaultAuthenticatorHost, kDefaultAuthenticatorPort);
}
Future<bool> isAuthenticatorPortFree() async => await pingAuthenticator(kDefaultAuthenticatorHost, kDefaultAuthenticatorPort.toString()) == null; Future<bool> isAuthenticatorPortFree() async => await pingAuthenticator(kDefaultAuthenticatorHost, kDefaultAuthenticatorPort) == null;
Future<bool> freeAuthenticatorPort() async { Future<bool> freeAuthenticatorPort() async {
await killProcessByPort(kDefaultAuthenticatorPort); await killProcessByPort(kDefaultAuthenticatorPort);
@@ -29,14 +27,14 @@ Future<bool> freeAuthenticatorPort() async {
return false; return false;
} }
Future<Uri?> pingAuthenticator(String host, String port, [bool https=false]) async { Future<Uri?> pingAuthenticator(String host, int port, [bool https=false]) async {
var hostName = _getHostName(host); var hostName = _getHostName(host);
var declaredScheme = _getScheme(host); var declaredScheme = _getScheme(host);
try{ try{
var uri = Uri( var uri = Uri(
scheme: declaredScheme ?? (https ? "https" : "http"), scheme: declaredScheme ?? (https ? "https" : "http"),
host: hostName, host: hostName,
port: int.parse(port), port: port,
path: "unknown" path: "unknown"
); );
var client = HttpClient() var client = HttpClient()

View File

@@ -5,18 +5,35 @@ import 'dart:isolate';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:dio/io.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'package:reboot_common/common.dart'; import 'package:reboot_common/common.dart';
const String kStopBuildDownloadSignal = "kill"; const String kStopBuildDownloadSignal = "kill";
final Dio _dio = Dio(); final Dio _dio = _buildDioInstance();
Dio _buildDioInstance() {
final dio = Dio();
final httpClientAdapter = dio.httpClientAdapter as IOHttpClientAdapter;
httpClientAdapter.createHttpClient = () {
final client = HttpClient();
client.badCertificateCallback = (X509Certificate cert, String host, int port) => true;
return client;
};
return dio;
}
final String _archiveSourceUrl = "https://raw.githubusercontent.com/simplyblk/Fortnitebuilds/main/README.md"; final String _archiveSourceUrl = "https://raw.githubusercontent.com/simplyblk/Fortnitebuilds/main/README.md";
final RegExp _rarProgressRegex = RegExp("^((100)|(\\d{1,2}(.\\d*)?))%\$"); final RegExp _rarProgressRegex = RegExp("^((100)|(\\d{1,2}(.\\d*)?))%\$");
const String _manifestSourceUrl = "https://manifest.fnbuilds.services"; const String _manifestSourceUrl = "https://manifest.fnbuilds.services";
const int _maxDownloadErrors = 30; const int _maxDownloadErrors = 30;
Future<List<FortniteBuild>> fetchBuilds(ignored) async { Future<List<FortniteBuild>> fetchBuilds(ignored) async {
(_dio.httpClientAdapter as IOHttpClientAdapter).createHttpClient = () =>
HttpClient()
..badCertificateCallback =
(X509Certificate cert, String host, int port) => true;
final results = await Future.wait([_fetchManifestBuilds(), _fetchArchiveBuilds()]); final results = await Future.wait([_fetchManifestBuilds(), _fetchArchiveBuilds()]);
final data = <FortniteBuild>[]; final data = <FortniteBuild>[];
for(final result in results) { for(final result in results) {
@@ -108,12 +125,6 @@ Future<void> downloadArchiveBuild(FortniteBuildDownloadOptions options) async {
final response = _downloadArchive(options, tempFile, startTime); final response = _downloadArchive(options, tempFile, startTime);
await Future.any([stopped.future, response]); await Future.any([stopped.future, response]);
if(!stopped.isCompleted) { if(!stopped.isCompleted) {
var awaitedResponse = await response;
if (!awaitedResponse.statusCode.toString().startsWith("20")) {
options.port.send("Erroneous status code: ${awaitedResponse.statusCode}");
return;
}
await _extractArchive(stopped, extension, tempFile, options); await _extractArchive(stopped, extension, tempFile, options);
} }
@@ -211,9 +222,23 @@ Future<Response> _downloadArchive(FortniteBuildDownloadOptions options, File tem
}, },
deleteOnError: false, deleteOnError: false,
options: Options( options: Options(
headers: byteStart == null ? null : { validateStatus: (statusCode) {
"Range": "bytes=${byteStart}-" if(statusCode == 200) {
return true;
} }
if(statusCode == 403 || statusCode == 503) {
throw Exception("The connection was denied: your firewall might be blocking the download");
}
throw Exception("The build downloader is not available right now");
},
headers: byteStart == null || byteStart <= 0 ? {
"Cookie": "_c_t_c=1"
} : {
"Cookie": "_c_t_c=1",
"Range": "bytes=${byteStart}-"
},
) )
); );
}catch(error) { }catch(error) {
@@ -226,17 +251,28 @@ Future<Response> _downloadArchive(FortniteBuildDownloadOptions options, File tem
} }
Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File tempFile, FortniteBuildDownloadOptions options) async { Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File tempFile, FortniteBuildDownloadOptions options) async {
var startTime = DateTime.now().millisecondsSinceEpoch; final startTime = DateTime.now().millisecondsSinceEpoch;
Process? process; Win32Process? process;
switch (extension.toLowerCase()) { switch (extension.toLowerCase()) {
case ".zip": case ".zip":
process = await Process.start( final sevenZip = File("${assetsDirectory.path}\\build\\7zip.exe");
"${assetsDirectory.path}\\build\\7zip.exe", if(!sevenZip.existsSync()) {
["a", "-bsp1", '-o"${options.destination.path}"', tempFile.path] throw "Corrupted installation: missing 7zip.exe";
}
process = await startProcess(
executable: sevenZip,
args: [
"x",
"-bsp1",
'-o"${options.destination.path}"',
"-y",
'"${tempFile.path}"'
],
); );
process.stdout.listen((bytes) { process.stdOutput.listen((data) {
print(data);
final now = DateTime.now().millisecondsSinceEpoch; final now = DateTime.now().millisecondsSinceEpoch;
final data = utf8.decode(bytes);
if(data == "Everything is Ok") { if(data == "Everything is Ok") {
options.port.send(FortniteBuildDownloadProgress(100, 0, true)); options.port.send(FortniteBuildDownloadProgress(100, 0, true));
return; return;
@@ -252,42 +288,45 @@ Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File
}); });
break; break;
case ".rar": case ".rar":
process = await Process.start( final winrar = File("${assetsDirectory.path}\\build\\winrar.exe");
"${assetsDirectory.path}\\build\\winrar.exe", if(!winrar.existsSync()) {
["x", "-o+", tempFile.path, "*.*", options.destination.path] throw "Corrupted installation: missing winrar.exe";
}
process = await startProcess(
executable: winrar,
args: [
"x",
"-o+",
tempFile.path,
"*.*",
options.destination.path
]
); );
process.stdout.listen((event) { process.stdOutput.listen((data) {
print(data);
final now = DateTime.now().millisecondsSinceEpoch; final now = DateTime.now().millisecondsSinceEpoch;
final data = utf8.decode(event); data.replaceAll("\r", "").replaceAll("\b", "").trim();
data.replaceAll("\r", "") if(data == "All OK") {
.replaceAll("\b", "") options.port.send(FortniteBuildDownloadProgress(100, 0, true));
.trim() return;
.split("\n") }
.forEach((entry) {
if(entry == "All OK") {
options.port.send(FortniteBuildDownloadProgress(100, 0, true));
return;
}
final element = _rarProgressRegex.firstMatch(entry)?.group(1); final element = _rarProgressRegex.firstMatch(data)?.group(1);
if(element == null) { if(element == null) {
return; return;
} }
final percentage = int.parse(element).toDouble(); final percentage = int.parse(element).toDouble();
_onProgress(startTime, now, percentage, true, options); _onProgress(startTime, now, percentage, true, options);
});
});
process.stderr.listen((event) {
final data = utf8.decode(event);
options.port.send(data);
}); });
process.errorOutput.listen((data) => options.port.send(data));
break; break;
default: default:
throw ArgumentError("Unexpected file extension: $extension}"); throw ArgumentError("Unexpected file extension: $extension}");
} }
await Future.any([stopped.future, process.exitCode]); await Future.any([stopped.future, watchProcess(process.pid)]);
} }
void _onProgress(int startTime, int now, double percentage, bool extracting, FortniteBuildDownloadOptions options) { void _onProgress(int startTime, int now, double percentage, bool extracting, FortniteBuildDownloadOptions options) {

View File

@@ -19,9 +19,13 @@ Future<bool> hasRebootDllUpdate(int? lastUpdateMs, {int hours = 24, bool force =
Future<void> downloadCriticalDll(String name, String outputPath) async { Future<void> downloadCriticalDll(String name, String outputPath) async {
final response = await http.get(Uri.parse("https://github.com/Auties00/reboot_launcher/tree/master/gui/assets/dlls/$name")); final response = await http.get(Uri.parse("https://github.com/Auties00/reboot_launcher/tree/master/gui/assets/dlls/$name"));
if(response.statusCode != 200) {
throw Exception("Cannot download $name: status code ${response.statusCode}");
}
final output = File(outputPath); final output = File(outputPath);
await output.parent.create(recursive: true); await output.parent.create(recursive: true);
await output.writeAsBytes(response.bodyBytes); await output.writeAsBytes(response.bodyBytes, flush: true);
} }
Future<int> downloadRebootDll(String url) async { Future<int> downloadRebootDll(String url) async {
@@ -29,12 +33,16 @@ Future<int> downloadRebootDll(String url) async {
final now = DateTime.now(); final now = DateTime.now();
try { try {
final response = await http.get(Uri.parse(url)); final response = await http.get(Uri.parse(url));
if(response.statusCode != 200) {
throw Exception("Cannot download reboot.zip: status code ${response.statusCode}");
}
outputDir = await installationDirectory.createTemp("reboot_out"); outputDir = await installationDirectory.createTemp("reboot_out");
final tempZip = File("${outputDir.path}\\reboot.zip"); final tempZip = File("${outputDir.path}\\reboot.zip");
await tempZip.writeAsBytes(response.bodyBytes); await tempZip.writeAsBytes(response.bodyBytes, flush: true);
await extractFileToDisk(tempZip.path, outputDir.path); await extractFileToDisk(tempZip.path, outputDir.path);
final rebootDll = File(outputDir.listSync().firstWhere((element) => path.extension(element.path) == ".dll").path); final rebootDll = File(outputDir.listSync().firstWhere((element) => path.extension(element.path) == ".dll").path);
await rebootDllFile.writeAsBytes(await rebootDll.readAsBytes()); await rebootDllFile.writeAsBytes(await rebootDll.readAsBytes(), flush: true);
return now.millisecondsSinceEpoch; return now.millisecondsSinceEpoch;
} finally{ } finally{
if(outputDir != null) { if(outputDir != null) {

View File

@@ -13,9 +13,10 @@ String? _lastIp;
String? _lastPort; String? _lastPort;
Semaphore _semaphore = Semaphore(); Semaphore _semaphore = Semaphore();
Future<int> startEmbeddedMatchmaker(bool detached) async => startBackgroundProcess( Future<Win32Process> startEmbeddedMatchmaker(bool detached) async => startProcess(
executable: matchmakerStartExecutable, executable: matchmakerStartExecutable,
window: detached window: detached,
); );
Stream<String?> watchMatchmakingIp() async* { Stream<String?> watchMatchmakingIp() async* {
@@ -74,7 +75,7 @@ Future<void> writeMatchmakingIp(String text) async {
await matchmakerConfigFile.writeAsString(config.toString(), flush: true); await matchmakerConfigFile.writeAsString(config.toString(), flush: true);
} }
Future<bool> isMatchmakerPortFree() async => await pingMatchmaker(kDefaultMatchmakerHost, kDefaultMatchmakerPort.toString()) == null; Future<bool> isMatchmakerPortFree() async => await pingMatchmaker(kDefaultMatchmakerHost, kDefaultMatchmakerPort) == null;
Future<bool> freeMatchmakerPort() async { Future<bool> freeMatchmakerPort() async {
await killProcessByPort(kDefaultMatchmakerPort); await killProcessByPort(kDefaultMatchmakerPort);
@@ -86,14 +87,14 @@ Future<bool> freeMatchmakerPort() async {
return false; return false;
} }
Future<Uri?> pingMatchmaker(String host, String port, [bool wss=false]) async { Future<Uri?> pingMatchmaker(String host, int port, [bool wss=false]) async {
var hostName = _getHostName(host); var hostName = _getHostName(host);
var declaredScheme = _getScheme(host); var declaredScheme = _getScheme(host);
try{ try{
var uri = Uri( var uri = Uri(
scheme: declaredScheme ?? (wss ? "wss" : "ws"), scheme: declaredScheme ?? (wss ? "wss" : "ws"),
host: hostName, host: hostName,
port: int.parse(port) port: port
); );
var completer = Completer<bool>(); var completer = Completer<bool>();
var socket = await WebSocket.connect(uri.toString()); var socket = await WebSocket.connect(uri.toString());

View File

@@ -72,7 +72,7 @@ extension FortniteVersionExtension on FortniteVersion {
return output; return output;
} }
File? get launcher => findExecutable(location, "FortniteLauncher.exe"); File? get launcherExecutable => findExecutable(location, "FortniteLauncher.exe");
File? get eacExecutable => findExecutable(location, "FortniteClient-Win64-Shipping_EAC.exe"); File? get eacExecutable => findExecutable(location, "FortniteClient-Win64-Shipping_EAC.exe");

View File

@@ -1,12 +1,14 @@
// ignore_for_file: non_constant_identifier_names // ignore_for_file: non_constant_identifier_names
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'dart:ffi'; import 'dart:ffi';
import 'dart:io'; import 'dart:io';
import 'dart:isolate'; import 'dart:isolate';
import 'dart:math'; import 'dart:math';
import 'package:ffi/ffi.dart'; import 'package:ffi/ffi.dart';
import 'package:reboot_common/src/model/process.dart';
import 'package:win32/win32.dart'; import 'package:win32/win32.dart';
import '../constant/game.dart'; import '../constant/game.dart';
@@ -30,15 +32,16 @@ final _CreateRemoteThread = _kernel32.lookupFunction<
Pointer lpParameter, Pointer lpParameter,
int dwCreationFlags, int dwCreationFlags,
Pointer<Uint32> lpThreadId)>('CreateRemoteThread'); Pointer<Uint32> lpThreadId)>('CreateRemoteThread');
const chunkSize = 1024;
Future<void> injectDll(int pid, String dll) async { Future<void> injectDll(int pid, String dll) async {
var process = OpenProcess( final process = OpenProcess(
0x43A, 0x43A,
0, 0,
pid pid
); );
var processAddress = GetProcAddress( final processAddress = GetProcAddress(
GetModuleHandle("KERNEL32".toNativeUtf16()), GetModuleHandle("KERNEL32".toNativeUtf16()),
"LoadLibraryA".toNativeUtf8() "LoadLibraryA".toNativeUtf8()
); );
@@ -47,7 +50,7 @@ Future<void> injectDll(int pid, String dll) async {
throw Exception("Cannot get process address for pid $pid"); throw Exception("Cannot get process address for pid $pid");
} }
var dllAddress = VirtualAllocEx( final dllAddress = VirtualAllocEx(
process, process,
nullptr, nullptr,
dll.length + 1, dll.length + 1,
@@ -55,7 +58,7 @@ Future<void> injectDll(int pid, String dll) async {
0x4 0x4
); );
var writeMemoryResult = WriteProcessMemory( final writeMemoryResult = WriteProcessMemory(
process, process,
dllAddress, dllAddress,
dll.toNativeUtf8(), dll.toNativeUtf8(),
@@ -67,7 +70,7 @@ Future<void> injectDll(int pid, String dll) async {
throw Exception("Memory write failed"); throw Exception("Memory write failed");
} }
var createThreadResult = _CreateRemoteThread( final createThreadResult = _CreateRemoteThread(
process, process,
nullptr, nullptr,
0, 0,
@@ -81,90 +84,152 @@ Future<void> injectDll(int pid, String dll) async {
throw Exception("Thread creation failed"); throw Exception("Thread creation failed");
} }
var closeResult = CloseHandle(process); final closeResult = CloseHandle(process);
if(closeResult != 1){ if(closeResult != 1){
throw Exception("Cannot close handle"); throw Exception("Cannot close handle");
} }
} }
bool runElevatedProcess(String executable, String args) { void _startProcess(_ProcessParameters params) {
final shellInput = calloc<SHELLEXECUTEINFO>(); final args = params.args;
shellInput.ref.lpFile = executable.toNativeUtf16(); final port = params.port;
shellInput.ref.lpParameters = args.toNativeUtf16(); final concatenatedArgs = args == null ? "" : " ${args.join(" ")}";
shellInput.ref.nShow = SW_HIDE; final command = params.window ? 'cmd.exe /k ""${params.executable.path}"$concatenatedArgs"' : '"${params.executable.path}"$concatenatedArgs';
shellInput.ref.fMask = ES_AWAYMODE_REQUIRED; print(command);
shellInput.ref.lpVerb = "runas".toNativeUtf16(); final processInfo = calloc<PROCESS_INFORMATION>();
shellInput.ref.cbSize = sizeOf<SHELLEXECUTEINFO>(); final lpStartupInfo = calloc<STARTUPINFO>();
final result = ShellExecuteEx(shellInput) == 1; lpStartupInfo.ref.cb = sizeOf<STARTUPINFO>();
free(shellInput); lpStartupInfo.ref.dwFlags |= STARTF_USESTDHANDLES;
return result; final securityAttributes = calloc<SECURITY_ATTRIBUTES>();
} securityAttributes.ref.nLength = sizeOf<SECURITY_ATTRIBUTES>();
securityAttributes.ref.bInheritHandle = TRUE;
void _startBackgroundProcess(_BackgroundProcessParameters params) { final hStdOutRead = calloc<HANDLE>();
var args = params.args; final hStdOutWrite = calloc<HANDLE>();
var concatenatedArgs = args == null ? "" : " ${args.map((entry) => '"$entry"').join(" ")}"; final hStdErrRead = calloc<HANDLE>();
var executablePath = TEXT('cmd.exe /k "${params.executable.path}"$concatenatedArgs'); final hStdErrWrite = calloc<HANDLE>();
var startupInfo = calloc<STARTUPINFO>(); if (CreatePipe(hStdOutRead, hStdOutWrite, securityAttributes, 0) == 0 || CreatePipe(hStdErrRead, hStdErrWrite, securityAttributes, 0) == 0) {
var processInfo = calloc<PROCESS_INFORMATION>(); final error = GetLastError();
var windowFlag = params.window ? CREATE_NEW_CONSOLE : CREATE_NO_WINDOW; port.send("Cannot create process pipe: $error");
var success = CreateProcess( return;
nullptr, }
executablePath,
nullptr, if(SetHandleInformation(hStdOutRead.value, HANDLE_FLAG_INHERIT, 0) == 0 || SetHandleInformation(hStdErrRead.value, HANDLE_FLAG_INHERIT, 0) == 0) {
nullptr, final error = GetLastError();
FALSE, port.send("Cannot set process pipe information: $error");
NORMAL_PRIORITY_CLASS | windowFlag | CREATE_NEW_PROCESS_GROUP,
nullptr,
TEXT(params.executable.parent.path),
startupInfo,
processInfo
);
if (success == 0) {
var error = GetLastError();
params.port.send("Cannot start process: $error");
return; return;
} }
var pid = processInfo.ref.dwProcessId; lpStartupInfo.ref.hStdOutput = hStdOutWrite.value;
free(startupInfo); lpStartupInfo.ref.hStdError = hStdErrWrite.value;
final success = CreateProcess(
nullptr,
TEXT(command),
nullptr,
nullptr,
TRUE,
NORMAL_PRIORITY_CLASS | (params.window ? CREATE_NEW_CONSOLE : CREATE_NO_WINDOW) | CREATE_NEW_PROCESS_GROUP,
nullptr,
TEXT(params.executable.parent.path),
lpStartupInfo,
processInfo
);
if (success == 0) {
final error = GetLastError();
port.send("Cannot start process: $error");
return;
}
CloseHandle(processInfo.ref.hProcess);
CloseHandle(processInfo.ref.hThread);
CloseHandle(hStdOutWrite.value);
CloseHandle(hStdErrWrite.value);
final pid = processInfo.ref.dwProcessId;
free(lpStartupInfo);
free(processInfo); free(processInfo);
params.port.send(pid); port.send(PrimitiveWin32Process(
pid: pid,
stdOutputHandle: hStdOutRead.value,
errorOutputHandle: hStdErrRead.value
));
} }
class _BackgroundProcessParameters { class _PipeReaderParams {
final int handle;
final SendPort port;
_PipeReaderParams(this.handle, this.port);
}
void _pipeToStreamChannelled(_PipeReaderParams params) {
final buf = calloc<Uint8>(chunkSize);
while(true) {
final bytesReadPtr = calloc<Uint32>();
final success = ReadFile(params.handle, buf, chunkSize, bytesReadPtr, nullptr);
if (success == FALSE) {
break;
}
final bytesRead = bytesReadPtr.value;
if (bytesRead == 0) {
break;
}
final lines = utf8.decode(buf.asTypedList(bytesRead)).split('\n');
for(final line in lines) {
params.port.send(line);
}
}
CloseHandle(params.handle);
free(buf);
Isolate.current.kill();
}
Stream<String> _pipeToStream(int pipeHandle) {
final port = ReceivePort();
Isolate.spawn(
_pipeToStreamChannelled,
_PipeReaderParams(pipeHandle, port.sendPort)
);
return port.map((event) => event as String);
}
class _ProcessParameters {
File executable; File executable;
List<String>? args; List<String>? args;
bool window; bool window;
SendPort port; SendPort port;
_BackgroundProcessParameters(this.executable, this.args, this.window, this.port); _ProcessParameters(this.executable, this.args, this.window, this.port);
} }
Future<int> startBackgroundProcess({required File executable, List<String>? args, bool window = false}) async { Future<Win32Process> startProcess({required File executable, List<String>? args, bool output = true, bool window = false}) async {
var completer = Completer<int>(); final completer = Completer<Win32Process>();
var port = ReceivePort(); final port = ReceivePort();
port.listen((message) => message is int ? completer.complete(message) : completer.completeError(message)); port.listen((message) {
var isolate = await Isolate.spawn( if(message is PrimitiveWin32Process) {
_startBackgroundProcess, completer.complete(Win32Process(
_BackgroundProcessParameters(executable, args, window, port.sendPort), pid: message.pid,
stdOutput: _pipeToStream(message.stdOutputHandle),
errorOutput: _pipeToStream(message.errorOutputHandle)
));
} else {
completer.completeError(message);
}
});
Isolate.spawn(
_startProcess,
_ProcessParameters(executable, args, window, port.sendPort),
errorsAreFatal: true errorsAreFatal: true
); );
var result = await completer.future; return await completer.future;
isolate.kill(priority: Isolate.immediate);
return result;
} }
int _NtResumeProcess(int hWnd) { final _NtResumeProcess = _ntdll.lookupFunction<Int32 Function(IntPtr hWnd),
final function = _ntdll.lookupFunction<Int32 Function(IntPtr hWnd), int Function(int hWnd)>('NtResumeProcess');
int Function(int hWnd)>('NtResumeProcess');
return function(hWnd);
}
int _NtSuspendProcess(int hWnd) { final _NtSuspendProcess = _ntdll.lookupFunction<Int32 Function(IntPtr hWnd),
final function = _ntdll.lookupFunction<Int32 Function(IntPtr hWnd), int Function(int hWnd)>('NtSuspendProcess');
int Function(int hWnd)>('NtSuspendProcess');
return function(hWnd);
}
bool suspend(int pid) { bool suspend(int pid) {
final processHandle = OpenProcess(PROCESS_SUSPEND_RESUME, FALSE, pid); final processHandle = OpenProcess(PROCESS_SUSPEND_RESUME, FALSE, pid);
@@ -214,7 +279,7 @@ List<String> createRebootArgs(String username, String password, bool host, bool
} }
password = password.isNotEmpty ? password : "Rebooted"; password = password.isNotEmpty ? password : "Rebooted";
var args = [ final args = [
"-epicapp=Fortnite", "-epicapp=Fortnite",
"-epicenv=Prod", "-epicenv=Prod",
"-epiclocale=en-us", "-epiclocale=en-us",

View File

@@ -16,7 +16,7 @@ class AuthenticatorController extends ServerController {
String get defaultHost => kDefaultAuthenticatorHost; String get defaultHost => kDefaultAuthenticatorHost;
@override @override
String get defaultPort => kDefaultAuthenticatorPort.toString(); int get defaultPort => kDefaultAuthenticatorPort;
@override @override
Future<bool> get isPortFree => isAuthenticatorPortFree(); Future<bool> get isPortFree => isAuthenticatorPortFree();
@@ -28,8 +28,8 @@ class AuthenticatorController extends ServerController {
RebootPageType get pageType => RebootPageType.authenticator; RebootPageType get pageType => RebootPageType.authenticator;
@override @override
Future<int> startEmbeddedInternal() => startEmbeddedAuthenticator(detached.value); Future<Win32Process> startEmbeddedInternal() => startEmbeddedAuthenticator(detached.value);
@override @override
Future<Uri?> pingServer(String host, String port) => pingAuthenticator(host, port); Future<Uri?> pingServer(String host, int port) => pingAuthenticator(host, port);
} }

View File

@@ -30,7 +30,7 @@ class HostingController extends GetxController {
description.addListener(() => _storage.write("description", description.text)); description.addListener(() => _storage.write("description", description.text));
password = TextEditingController(text: _storage.read("password") ?? ""); password = TextEditingController(text: _storage.read("password") ?? "");
password.addListener(() => _storage.write("password", password.text)); password.addListener(() => _storage.write("password", password.text));
discoverable = RxBool(_storage.read("discoverable") ?? true); discoverable = RxBool(_storage.read("discoverable") ?? false);
discoverable.listen((value) => _storage.write("discoverable", value)); discoverable.listen((value) => _storage.write("discoverable", value));
headless = RxBool(_storage.read("headless") ?? true); headless = RxBool(_storage.read("headless") ?? true);
headless.listen((value) => _storage.write("headless", value)); headless.listen((value) => _storage.write("headless", value));

View File

@@ -45,7 +45,7 @@ class MatchmakerController extends ServerController {
String get defaultHost => kDefaultMatchmakerHost; String get defaultHost => kDefaultMatchmakerHost;
@override @override
String get defaultPort => kDefaultMatchmakerPort.toString(); int get defaultPort => kDefaultMatchmakerPort;
@override @override
Future<bool> get isPortFree => isMatchmakerPortFree(); Future<bool> get isPortFree => isMatchmakerPortFree();
@@ -57,8 +57,8 @@ class MatchmakerController extends ServerController {
RebootPageType get pageType => RebootPageType.matchmaker; RebootPageType get pageType => RebootPageType.matchmaker;
@override @override
Future<int> startEmbeddedInternal() => startEmbeddedMatchmaker(detached.value); Future<Win32Process> startEmbeddedInternal() => startEmbeddedMatchmaker(detached.value);
@override @override
Future<Uri?> pingServer(String host, String port) => pingMatchmaker(host, port); Future<Uri?> pingServer(String host, int port) => pingMatchmaker(host, port);
} }

View File

@@ -51,9 +51,9 @@ abstract class ServerController extends GetxController {
String get defaultHost; String get defaultHost;
String get defaultPort; int get defaultPort;
Future<Uri?> pingServer(String host, String port); Future<Uri?> pingServer(String host, int port);
Future<bool> get isPortFree; Future<bool> get isPortFree;
@@ -64,17 +64,17 @@ abstract class ServerController extends GetxController {
Future<bool> freePort(); Future<bool> freePort();
@protected @protected
Future<int> startEmbeddedInternal(); Future<Win32Process> startEmbeddedInternal();
void reset() async { void reset() async {
type.value = ServerType.values.elementAt(0); type.value = ServerType.values.elementAt(0);
for (var type in ServerType.values) { for (final type in ServerType.values) {
storage.write("${type.name}_host", null); storage.write("${type.name}_host", null);
storage.write("${type.name}_port", null); storage.write("${type.name}_port", null);
} }
host.text = type.value != ServerType.remote ? defaultHost : ""; host.text = type.value != ServerType.remote ? defaultHost : "";
port.text = defaultPort; port.text = defaultPort.toString();
detached.value = false; detached.value = false;
} }
@@ -85,48 +85,48 @@ abstract class ServerController extends GetxController {
} }
String _readPort() => String _readPort() =>
storage.read("${type.value.name}_port") ?? defaultPort; storage.read("${type.value.name}_port") ?? defaultPort.toString();
Stream<ServerResult> start() async* { Stream<ServerResult> start() async* {
if(started.value) {
return;
}
if(type() != ServerType.local) {
started.value = true;
yield ServerResult(ServerResultType.starting);
}else {
started.value = false;
if(port != defaultPort) {
yield ServerResult(ServerResultType.starting);
}
}
try { try {
var host = this.host.text.trim(); if(started.value) {
if (host.isEmpty) { return;
}
final hostData = this.host.text.trim();
final portData = this.port.text.trim();
if(type() != ServerType.local) {
started.value = true;
yield ServerResult(ServerResultType.starting);
}else {
started.value = false;
if(portData != defaultPort) {
yield ServerResult(ServerResultType.starting);
}
}
if (hostData.isEmpty) {
yield ServerResult(ServerResultType.missingHostError); yield ServerResult(ServerResultType.missingHostError);
started.value = false; started.value = false;
return; return;
} }
var port = this.port.text.trim(); if (portData.isEmpty) {
if (port.isEmpty) {
yield ServerResult(ServerResultType.missingPortError); yield ServerResult(ServerResultType.missingPortError);
started.value = false; started.value = false;
return; return;
} }
var portNumber = int.tryParse(port); final portNumber = int.tryParse(portData);
if (portNumber == null) { if (portNumber == null) {
yield ServerResult(ServerResultType.illegalPortError); yield ServerResult(ServerResultType.illegalPortError);
started.value = false; started.value = false;
return; return;
} }
if ((type() != ServerType.local || port != defaultPort) && await isPortTaken) { if ((type() != ServerType.local || portData != defaultPort) && await isPortTaken) {
yield ServerResult(ServerResultType.freeingPort); yield ServerResult(ServerResultType.freeingPort);
var result = await freePort(); final result = await freePort();
yield ServerResult(result ? ServerResultType.freePortSuccess : ServerResultType.freePortError); yield ServerResult(result ? ServerResultType.freePortSuccess : ServerResultType.freePortError);
if(!result) { if(!result) {
started.value = false; started.value = false;
@@ -136,8 +136,15 @@ abstract class ServerController extends GetxController {
switch(type()){ switch(type()){
case ServerType.embedded: case ServerType.embedded:
final pid = await startEmbeddedInternal(); final process = await startEmbeddedInternal();
watchProcess(pid).then((value) { final processPid = process.pid;
if(processPid == null) {
yield ServerResult(ServerResultType.startError);
started.value = false;
return;
}
watchProcess(processPid).then((value) {
if(started()) { if(started()) {
started.value = false; started.value = false;
} }
@@ -145,7 +152,7 @@ abstract class ServerController extends GetxController {
break; break;
case ServerType.remote: case ServerType.remote:
yield ServerResult(ServerResultType.pingingRemote); yield ServerResult(ServerResultType.pingingRemote);
var uriResult = await pingServer(host, port); final uriResult = await pingServer(hostData, portNumber);
if(uriResult == null) { if(uriResult == null) {
yield ServerResult(ServerResultType.pingError); yield ServerResult(ServerResultType.pingError);
started.value = false; started.value = false;
@@ -155,7 +162,7 @@ abstract class ServerController extends GetxController {
remoteServer = await startRemoteAuthenticatorProxy(uriResult); remoteServer = await startRemoteAuthenticatorProxy(uriResult);
break; break;
case ServerType.local: case ServerType.local:
if(port != defaultPort) { if(portData != defaultPort) {
localServer = await startRemoteAuthenticatorProxy(Uri.parse("http://$defaultHost:$port")); localServer = await startRemoteAuthenticatorProxy(Uri.parse("http://$defaultHost:$port"));
} }
@@ -163,7 +170,7 @@ abstract class ServerController extends GetxController {
} }
yield ServerResult(ServerResultType.pingingLocal); yield ServerResult(ServerResultType.pingingLocal);
var uriResult = await pingServer(defaultHost, defaultPort); final uriResult = await pingServer(defaultHost, defaultPort);
if(uriResult == null) { if(uriResult == null) {
yield ServerResult(ServerResultType.pingError); yield ServerResult(ServerResultType.pingError);
remoteServer?.close(force: true); remoteServer?.close(force: true);
@@ -195,7 +202,7 @@ abstract class ServerController extends GetxController {
try{ try{
switch(type()){ switch(type()){
case ServerType.embedded: case ServerType.embedded:
killProcessByPort(int.parse(defaultPort)); killProcessByPort(defaultPort);
break; break;
case ServerType.remote: case ServerType.remote:
await remoteServer?.close(force: true); await remoteServer?.close(force: true);

View File

@@ -24,6 +24,7 @@ extension ServerControllerDialog on ServerController {
var stream = toggle(); var stream = toggle();
var completer = Completer<bool>(); var completer = Completer<bool>();
worker = stream.listen((event) { worker = stream.listen((event) {
print(event.type);
switch (event.type) { switch (event.type) {
case ServerResultType.starting: case ServerResultType.starting:
showInfoBar( showInfoBar(

View File

@@ -28,7 +28,7 @@ extension GameInstanceWatcher on GameInstance {
} }
}); });
observerPid = await startBackgroundProcess( final process = await startProcess(
executable: _executable, executable: _executable,
args: [ args: [
hostingController.uuid, hostingController.uuid,
@@ -36,8 +36,10 @@ extension GameInstanceWatcher on GameInstance {
launcherPid?.toString() ?? "-1", launcherPid?.toString() ?? "-1",
eacPid?.toString() ?? "-1", eacPid?.toString() ?? "-1",
hosting.toString() hosting.toString()
] ],
); );
observerPid = process.pid;
} }
bool get _nestedHosting { bool get _nestedHosting {

View File

@@ -11,7 +11,7 @@ import 'package:reboot_launcher/src/util/translations.dart';
final UpdateController _updateController = Get.find<UpdateController>(); final UpdateController _updateController = Get.find<UpdateController>();
Future<void> downloadCriticalDllInteractive(String filePath) async { Future<void> downloadCriticalDllInteractive(String filePath) async {
try { try {
final fileName = path.basename(filePath); final fileName = path.basename(filePath).toLowerCase();
if (fileName == "reboot.dll") { if (fileName == "reboot.dll") {
_updateController.update(true); _updateController.update(true);
return; return;

View File

@@ -51,11 +51,11 @@ class _LaunchButtonState extends State<LaunchButton> {
child: Obx(() => SizedBox( child: Obx(() => SizedBox(
height: 48, height: 48,
child: Button( child: Button(
onPressed: () => _operation = CancelableOperation.fromFuture(_start()), onPressed: () => _operation = CancelableOperation.fromFuture(_start()),
child: Align( child: Align(
alignment: Alignment.center, alignment: Alignment.center,
child: Text(_hasStarted ? _stopMessage : _startMessage) child: Text(_hasStarted ? _stopMessage : _startMessage)
) )
), ),
)), )),
), ),
@@ -72,11 +72,11 @@ class _LaunchButtonState extends State<LaunchButton> {
Future<void> _start() async { Future<void> _start() async {
if (_hasStarted) { if (_hasStarted) {
_onStop( _onStop(
reason: _StopReason.normal reason: _StopReason.normal
); );
return; return;
} }
if(_operation != null) { if(_operation != null) {
return; return;
} }
@@ -149,7 +149,7 @@ class _LaunchButtonState extends State<LaunchButton> {
if(_hostingController.started()){ if(_hostingController.started()){
return null; return null;
} }
if(!_hostingController.automaticServer()) { if(!_hostingController.automaticServer()) {
return null; return null;
} }
@@ -160,10 +160,12 @@ class _LaunchButtonState extends State<LaunchButton> {
} }
Future<GameInstance?> _startGameProcesses(FortniteVersion version, bool host, GameInstance? linkedHosting) async { Future<GameInstance?> _startGameProcesses(FortniteVersion version, bool host, GameInstance? linkedHosting) async {
final launcherProcess = await _createLauncherProcess(version); final launcherProcess = await _createPausedProcess(version.launcherExecutable);
final eacProcess = await _createEacProcess(version); print("Created launcher process");
final eacProcess = await _createPausedProcess(version.eacExecutable);
print("Created eac process");
final executable = await version.executable; final executable = await version.executable;
final gameProcess = await _createGameProcess(executable!.path, host); final gameProcess = await _createGameProcess(executable!, host);
if(gameProcess == null) { if(gameProcess == null) {
return null; return null;
} }
@@ -187,7 +189,7 @@ class _LaunchButtonState extends State<LaunchButton> {
return instance; return instance;
} }
Future<int?> _createGameProcess(String gamePath, bool host) async { Future<int?> _createGameProcess(File executable, bool host) async {
if(!_hasStarted) { if(!_hasStarted) {
return null; return null;
} }
@@ -199,38 +201,32 @@ class _LaunchButtonState extends State<LaunchButton> {
_hostingController.headless.value, _hostingController.headless.value,
_gameController.customLaunchArgs.text _gameController.customLaunchArgs.text
); );
final gameProcess = await Process.start( final gameProcess = await startProcess(
gamePath, executable: executable,
gameArgs args: gameArgs,
window: false
); );
gameProcess gameProcess.stdOutput.listen((line) => _onGameOutput(line, host, false));
..exitCode.then((_) => _onStop(reason: _StopReason.exitCode)) gameProcess.errorOutput.listen((line) => _onGameOutput(line, host, true));
..outLines.forEach((line) => _onGameOutput(line, host, false)) watchProcess(gameProcess.pid).then((_) => _onStop(reason: _StopReason.exitCode));
..errLines.forEach((line) => _onGameOutput(line, host, true));
return gameProcess.pid; return gameProcess.pid;
} }
Future<int?> _createLauncherProcess(FortniteVersion version) async { Future<int?> _createPausedProcess(File? file) async {
final launcherFile = version.launcher; if (file == null) {
if (launcherFile == null) {
return null; return null;
} }
final launcherProcess = await Process.start(launcherFile.path, []); final process = await startProcess(
final pid = launcherProcess.pid; executable: file,
suspend(pid); args: [],
return pid; window: false,
} output: false
);
Future<int?> _createEacProcess(FortniteVersion version) async { print("Started process: ${process.pid}");
final eacFile = version.eacExecutable; final pid = process.pid;
if (eacFile == null) {
return null;
}
final eacProcess = await Process.start(eacFile.path, []);
final pid = eacProcess.pid;
suspend(pid); suspend(pid);
print("Suspended");
return pid; return pid;
} }
@@ -473,7 +469,7 @@ class _LaunchButtonState extends State<LaunchButton> {
return _settingsController.memoryLeakDll.text; return _settingsController.memoryLeakDll.text;
} }
} }
Future<File?> _getDllFileOrStop(_Injectable injectable, bool host) async { Future<File?> _getDllFileOrStop(_Injectable injectable, bool host) async {
final path = _getDllPath(injectable); final path = _getDllPath(injectable);
final file = File(path); final file = File(path);

12
gui/lib/test.dart Normal file
View File

@@ -0,0 +1,12 @@
import 'dart:io';
import 'package:reboot_common/common.dart';
void main() async {
final process = await startProcess(
executable: File("C:\\FortniteBuilds\\Fortnite 4.2\\Fortnite 4.2\\Fortnite1\\FortniteGame\\Binaries\\Win64\\FortniteClient-Win64-Shipping-Reboot.exe"),
args: "-epicapp=Fortnite -epicenv=Prod -epiclocale=en-us -epicportal -skippatchcheck -nobe -fromfl=eac -fltoken=3db3ba5dcbd2e16703f3978d -caldera=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50X2lkIjoiYmU5ZGE1YzJmYmVhNDQwN2IyZjQwZWJhYWQ4NTlhZDQiLCJnZW5lcmF0ZWQiOjE2Mzg3MTcyNzgsImNhbGRlcmFHdWlkIjoiMzgxMGI4NjMtMmE2NS00NDU3LTliNTgtNGRhYjNiNDgyYTg2IiwiYWNQcm92aWRlciI6IkVhc3lBbnRpQ2hlYXQiLCJub3RlcyI6IiIsImZhbGxiYWNrIjpmYWxzZX0.VAWQB67RTxhiWOxx7DBjnzDnXyyEnX7OljJm-j2d88G_WgwQ9wrE6lwMEHZHjBd1ISJdUO1UVUqkfLdU5nofBQ -AUTH_LOGIN=Player698@projectreboot.dev -AUTH_PASSWORD=Rebooted -AUTH_TYPE=epic -nullrhi -nosplash -nosound".split(" ")
);
process.stdOutput.listen((event) => stdout.writeln(event));
process.errorOutput.listen((event) => stdout.writeln(event));
}