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

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

View File

@@ -5,18 +5,35 @@ import 'dart:isolate';
import 'dart:typed_data';
import 'package:dio/dio.dart';
import 'package:dio/io.dart';
import 'package:path/path.dart' as path;
import 'package:reboot_common/common.dart';
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 RegExp _rarProgressRegex = RegExp("^((100)|(\\d{1,2}(.\\d*)?))%\$");
const String _manifestSourceUrl = "https://manifest.fnbuilds.services";
const int _maxDownloadErrors = 30;
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 data = <FortniteBuild>[];
for(final result in results) {
@@ -108,12 +125,6 @@ Future<void> downloadArchiveBuild(FortniteBuildDownloadOptions options) async {
final response = _downloadArchive(options, tempFile, startTime);
await Future.any([stopped.future, response]);
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);
}
@@ -211,9 +222,23 @@ Future<Response> _downloadArchive(FortniteBuildDownloadOptions options, File tem
},
deleteOnError: false,
options: Options(
headers: byteStart == null ? null : {
"Range": "bytes=${byteStart}-"
validateStatus: (statusCode) {
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) {
@@ -226,17 +251,28 @@ Future<Response> _downloadArchive(FortniteBuildDownloadOptions options, File tem
}
Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File tempFile, FortniteBuildDownloadOptions options) async {
var startTime = DateTime.now().millisecondsSinceEpoch;
Process? process;
final startTime = DateTime.now().millisecondsSinceEpoch;
Win32Process? process;
switch (extension.toLowerCase()) {
case ".zip":
process = await Process.start(
"${assetsDirectory.path}\\build\\7zip.exe",
["a", "-bsp1", '-o"${options.destination.path}"', tempFile.path]
final sevenZip = File("${assetsDirectory.path}\\build\\7zip.exe");
if(!sevenZip.existsSync()) {
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 data = utf8.decode(bytes);
if(data == "Everything is Ok") {
options.port.send(FortniteBuildDownloadProgress(100, 0, true));
return;
@@ -252,42 +288,45 @@ Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File
});
break;
case ".rar":
process = await Process.start(
"${assetsDirectory.path}\\build\\winrar.exe",
["x", "-o+", tempFile.path, "*.*", options.destination.path]
final winrar = File("${assetsDirectory.path}\\build\\winrar.exe");
if(!winrar.existsSync()) {
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 data = utf8.decode(event);
data.replaceAll("\r", "")
.replaceAll("\b", "")
.trim()
.split("\n")
.forEach((entry) {
if(entry == "All OK") {
options.port.send(FortniteBuildDownloadProgress(100, 0, true));
return;
}
data.replaceAll("\r", "").replaceAll("\b", "").trim();
if(data == "All OK") {
options.port.send(FortniteBuildDownloadProgress(100, 0, true));
return;
}
final element = _rarProgressRegex.firstMatch(entry)?.group(1);
if(element == null) {
return;
}
final element = _rarProgressRegex.firstMatch(data)?.group(1);
if(element == null) {
return;
}
final percentage = int.parse(element).toDouble();
_onProgress(startTime, now, percentage, true, options);
});
});
process.stderr.listen((event) {
final data = utf8.decode(event);
options.port.send(data);
final percentage = int.parse(element).toDouble();
_onProgress(startTime, now, percentage, true, options);
});
process.errorOutput.listen((data) => options.port.send(data));
break;
default:
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) {

View File

@@ -19,9 +19,13 @@ Future<bool> hasRebootDllUpdate(int? lastUpdateMs, {int hours = 24, bool force =
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"));
if(response.statusCode != 200) {
throw Exception("Cannot download $name: status code ${response.statusCode}");
}
final output = File(outputPath);
await output.parent.create(recursive: true);
await output.writeAsBytes(response.bodyBytes);
await output.writeAsBytes(response.bodyBytes, flush: true);
}
Future<int> downloadRebootDll(String url) async {
@@ -29,12 +33,16 @@ Future<int> downloadRebootDll(String url) async {
final now = DateTime.now();
try {
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");
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);
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;
} finally{
if(outputDir != null) {

View File

@@ -13,9 +13,10 @@ String? _lastIp;
String? _lastPort;
Semaphore _semaphore = Semaphore();
Future<int> startEmbeddedMatchmaker(bool detached) async => startBackgroundProcess(
Future<Win32Process> startEmbeddedMatchmaker(bool detached) async => startProcess(
executable: matchmakerStartExecutable,
window: detached
window: detached,
);
Stream<String?> watchMatchmakingIp() async* {
@@ -74,7 +75,7 @@ Future<void> writeMatchmakingIp(String text) async {
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 {
await killProcessByPort(kDefaultMatchmakerPort);
@@ -86,14 +87,14 @@ Future<bool> freeMatchmakerPort() async {
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 declaredScheme = _getScheme(host);
try{
var uri = Uri(
scheme: declaredScheme ?? (wss ? "wss" : "ws"),
host: hostName,
port: int.parse(port)
port: port
);
var completer = Completer<bool>();
var socket = await WebSocket.connect(uri.toString());

View File

@@ -72,7 +72,7 @@ extension FortniteVersionExtension on FortniteVersion {
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");

View File

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