mirror of
https://github.com/Auties00/Reboot-Launcher.git
synced 2026-01-13 11:12:23 +01:00
<feat: New project structure>
<feat: New release>
This commit is contained in:
79
common/lib/src/util/authenticator.dart
Normal file
79
common/lib/src/util/authenticator.dart
Normal file
@@ -0,0 +1,79 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:process_run/process_run.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:shelf/shelf_io.dart';
|
||||
import 'package:shelf_proxy/shelf_proxy.dart';
|
||||
|
||||
|
||||
final authenticatorLogFile = File("${logsDirectory.path}\\authenticator.log");
|
||||
final authenticatorDirectory = Directory("${assetsDirectory.path}\\lawin");
|
||||
final authenticatorExecutable = File("${authenticatorDirectory.path}\\run.bat");
|
||||
|
||||
Future<Process> startEmbeddedAuthenticator(bool detached) async {
|
||||
if(!authenticatorExecutable.existsSync()) {
|
||||
throw StateError("${authenticatorExecutable.path} doesn't exist");
|
||||
}
|
||||
|
||||
var process = await Process.start(
|
||||
authenticatorExecutable.path,
|
||||
[],
|
||||
workingDirectory: authenticatorDirectory.path,
|
||||
mode: detached ? ProcessStartMode.detached : ProcessStartMode.normal
|
||||
);
|
||||
if(!detached) {
|
||||
authenticatorLogFile.createSync(recursive: true);
|
||||
process.outLines.forEach((element) => authenticatorLogFile.writeAsStringSync("$element\n", mode: FileMode.append));
|
||||
process.errLines.forEach((element) => authenticatorLogFile.writeAsStringSync("$element\n", mode: FileMode.append));
|
||||
}
|
||||
return process;
|
||||
}
|
||||
|
||||
Future<HttpServer> startRemoteAuthenticatorProxy(Uri uri) async => await serve(proxyHandler(uri), kDefaultAuthenticatorHost, int.parse(kDefaultAuthenticatorPort));
|
||||
|
||||
Future<bool> isAuthenticatorPortFree() async => isPortFree(int.parse(kDefaultAuthenticatorPort));
|
||||
|
||||
Future<bool> freeAuthenticatorPort() async {
|
||||
var releaseBat = File("${assetsDirectory.path}\\lawin\\kill_lawin.bat");
|
||||
await Process.run(releaseBat.path, []);
|
||||
var standardResult = await isAuthenticatorPortFree();
|
||||
if(standardResult) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var elevatedResult = await runElevatedProcess(releaseBat.path, "");
|
||||
if(!elevatedResult) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return await isAuthenticatorPortFree();
|
||||
}
|
||||
|
||||
Future<Uri?> pingSelf(String port) async => ping(kDefaultAuthenticatorHost, port);
|
||||
|
||||
Future<Uri?> ping(String host, String 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),
|
||||
path: "unknown"
|
||||
);
|
||||
var client = HttpClient()
|
||||
..connectionTimeout = const Duration(seconds: 5);
|
||||
var request = await client.getUrl(uri);
|
||||
var response = await request.close();
|
||||
var body = utf8.decode(await response.single);
|
||||
return body.contains("epicgames") || body.contains("lawinserver") ? uri : null;
|
||||
}catch(_){
|
||||
return https || declaredScheme != null ? null : await ping(host, port, true);
|
||||
}
|
||||
}
|
||||
|
||||
String? _getHostName(String host) => host.replaceFirst("http://", "").replaceFirst("https://", "");
|
||||
|
||||
String? _getScheme(String host) => host.startsWith("http://") ? "http" : host.startsWith("https://") ? "https" : null;
|
||||
|
||||
152
common/lib/src/util/build.dart
Normal file
152
common/lib/src/util/build.dart
Normal file
@@ -0,0 +1,152 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:reboot_common/common.dart';
|
||||
|
||||
final Uri _manifestSourceUrl = Uri.parse(
|
||||
"https://raw.githubusercontent.com/simplyblk/Fortnitebuilds/main/README.md");
|
||||
|
||||
Future<List<FortniteBuild>> fetchBuilds(ignored) async {
|
||||
var response = await http.get(_manifestSourceUrl);
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception("Erroneous status code: ${response.statusCode}");
|
||||
}
|
||||
|
||||
var results = <FortniteBuild>[];
|
||||
for (var line in response.body.split("\n")) {
|
||||
if(!line.startsWith("|")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var parts = line.substring(1, line.length - 1).split("|");
|
||||
if(parts.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var link = parts.last.trim();
|
||||
if(!link.endsWith(".zip") && !link.endsWith(".rar")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var version = parts.first.trim();
|
||||
version = version.substring(0, version.indexOf("-"));
|
||||
results.add(FortniteBuild(version: "Fortnite $version", link: link));
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
|
||||
Future<void> downloadArchiveBuild(ArchiveDownloadOptions options) async {
|
||||
var stopped = _setupLifecycle(options);
|
||||
var outputDir = Directory("${options.destination.path}\\.build");
|
||||
outputDir.createSync(recursive: true);
|
||||
try {
|
||||
options.destination.createSync(recursive: true);
|
||||
var fileName = options.archiveUrl.substring(options.archiveUrl.lastIndexOf("/") + 1);
|
||||
var extension = path.extension(fileName);
|
||||
var tempFile = File("${outputDir.path}\\$fileName");
|
||||
if(tempFile.existsSync()) {
|
||||
tempFile.deleteSync(recursive: true);
|
||||
}
|
||||
|
||||
await _download(options, tempFile, stopped);
|
||||
await _extract(stopped, extension, tempFile, options);
|
||||
delete(outputDir);
|
||||
} catch(message) {
|
||||
throw Exception("Cannot download build: $message");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _download(ArchiveDownloadOptions options, File tempFile, Completer<dynamic> stopped) async {
|
||||
var client = http.Client();
|
||||
var request = http.Request("GET", Uri.parse(options.archiveUrl));
|
||||
request.headers['Connection'] = 'Keep-Alive';
|
||||
var response = await client.send(request);
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception("Erroneous status code: ${response.statusCode}");
|
||||
}
|
||||
|
||||
var startTime = DateTime.now().millisecondsSinceEpoch;
|
||||
var length = response.contentLength!;
|
||||
var received = 0;
|
||||
var sink = tempFile.openWrite();
|
||||
var subscription = response.stream.listen((data) async {
|
||||
received += data.length;
|
||||
var now = DateTime.now();
|
||||
var progress = (received / length) * 100;
|
||||
var msLeft = startTime + (now.millisecondsSinceEpoch - startTime) * length / received - now.millisecondsSinceEpoch;
|
||||
var minutesLeft = (msLeft / 1000 / 60).round();
|
||||
options.port.send(ArchiveDownloadProgress(progress, minutesLeft, false));
|
||||
sink.add(data);
|
||||
});
|
||||
|
||||
await Future.any([stopped.future, subscription.asFuture()]);
|
||||
if(stopped.isCompleted) {
|
||||
await subscription.cancel();
|
||||
}else {
|
||||
await sink.flush();
|
||||
await sink.close();
|
||||
await sink.done;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _extract(Completer<dynamic> stopped, String extension, File tempFile, ArchiveDownloadOptions options) async {
|
||||
if(stopped.isCompleted) {
|
||||
return;
|
||||
}
|
||||
|
||||
options.port.send(ArchiveDownloadProgress(0, -1, true));
|
||||
Process? process;
|
||||
switch (extension.toLowerCase()) {
|
||||
case '.zip':
|
||||
process = await Process.start(
|
||||
'tar',
|
||||
['-xf', tempFile.path, '-C', options.destination.path],
|
||||
mode: ProcessStartMode.inheritStdio
|
||||
);
|
||||
break;
|
||||
case '.rar':
|
||||
process = await Process.start(
|
||||
'${assetsDirectory.path}\\misc\\winrar.exe',
|
||||
['x', tempFile.path, '*.*', options.destination.path],
|
||||
mode: ProcessStartMode.inheritStdio
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw ArgumentError("Unexpected file extension: $extension}");
|
||||
}
|
||||
|
||||
await Future.any([stopped.future, process.exitCode]);
|
||||
}
|
||||
|
||||
Completer<dynamic> _setupLifecycle(ArchiveDownloadOptions options) {
|
||||
var stopped = Completer();
|
||||
var lifecyclePort = ReceivePort();
|
||||
lifecyclePort.listen((message) {
|
||||
if(message == "kill") {
|
||||
stopped.complete();
|
||||
}
|
||||
});
|
||||
options.port.send(lifecyclePort.sendPort);
|
||||
return stopped;
|
||||
}
|
||||
|
||||
class ArchiveDownloadOptions {
|
||||
String archiveUrl;
|
||||
Directory destination;
|
||||
SendPort port;
|
||||
|
||||
ArchiveDownloadOptions(this.archiveUrl, this.destination, this.port);
|
||||
}
|
||||
|
||||
class ArchiveDownloadProgress {
|
||||
final double progress;
|
||||
final int minutesLeft;
|
||||
final bool extracting;
|
||||
|
||||
ArchiveDownloadProgress(this.progress, this.minutesLeft, this.extracting);
|
||||
}
|
||||
38
common/lib/src/util/matchmaker.dart
Normal file
38
common/lib/src/util/matchmaker.dart
Normal file
@@ -0,0 +1,38 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:ini/ini.dart';
|
||||
|
||||
import 'package:reboot_common/common.dart';
|
||||
|
||||
Future<void> writeMatchmakingIp(String text) async {
|
||||
var file = File("${assetsDirectory.path}\\lawin\\Config\\config.ini");
|
||||
if(!file.existsSync()){
|
||||
return;
|
||||
}
|
||||
|
||||
var splitIndex = text.indexOf(":");
|
||||
var ip = splitIndex != -1 ? text.substring(0, splitIndex) : text;
|
||||
var port = splitIndex != -1 ? text.substring(splitIndex + 1) : "7777";
|
||||
var config = Config.fromString(file.readAsStringSync());
|
||||
config.set("GameServer", "ip", ip);
|
||||
config.set("GameServer", "port", port);
|
||||
file.writeAsStringSync(config.toString());
|
||||
}
|
||||
|
||||
Future<bool> isMatchmakerPortFree() async => isPortFree(int.parse(kDefaultMatchmakerPort));
|
||||
|
||||
Future<bool> freeMatchmakerPort() async {
|
||||
var releaseBat = File("${assetsDirectory.path}\\lawin\\kill_matchmaker.bat");
|
||||
await Process.run(releaseBat.path, []);
|
||||
var standardResult = await isMatchmakerPortFree();
|
||||
if(standardResult) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var elevatedResult = await runElevatedProcess(releaseBat.path, "");
|
||||
if(!elevatedResult) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return await isMatchmakerPortFree();
|
||||
}
|
||||
22
common/lib/src/util/network.dart
Normal file
22
common/lib/src/util/network.dart
Normal file
@@ -0,0 +1,22 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:reboot_common/common.dart';
|
||||
|
||||
bool isLocalHost(String host) => host.trim() == "127.0.0.1"
|
||||
|| host.trim().toLowerCase() == "localhost"
|
||||
|| host.trim() == "0.0.0.0";
|
||||
|
||||
Future<bool> isPortFree(int port) async {
|
||||
try {
|
||||
final server = await ServerSocket.bind(InternetAddress.anyIPv4, port);
|
||||
await server.close();
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> resetWinNat() async {
|
||||
var binary = File("${authenticatorDirectory.path}\\winnat.bat");
|
||||
await runElevatedProcess(binary.path, "");
|
||||
}
|
||||
58
common/lib/src/util/patcher.dart
Normal file
58
common/lib/src/util/patcher.dart
Normal file
@@ -0,0 +1,58 @@
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
final Uint8List _originalHeadless = Uint8List.fromList([
|
||||
45, 0, 105, 0, 110, 0, 118, 0, 105, 0, 116, 0, 101, 0, 115, 0, 101, 0, 115, 0, 115, 0, 105, 0, 111, 0, 110, 0, 32, 0, 45, 0, 105, 0, 110, 0, 118, 0, 105, 0, 116, 0, 101, 0, 102, 0, 114, 0, 111, 0, 109, 0, 32, 0, 45, 0, 112, 0, 97, 0, 114, 0, 116, 0, 121, 0, 95, 0, 106, 0, 111, 0, 105, 0, 110, 0, 105, 0, 110, 0, 102, 0, 111, 0, 95, 0, 116, 0, 111, 0, 107, 0, 101, 0, 110, 0, 32, 0, 45, 0, 114, 0, 101, 0, 112, 0, 108, 0, 97, 0, 121, 0
|
||||
]);
|
||||
|
||||
final Uint8List _patchedHeadless = Uint8List.fromList([
|
||||
45, 0, 108, 0, 111, 0, 103, 0, 32, 0, 45, 0, 110, 0, 111, 0, 115, 0, 112, 0, 108, 0, 97, 0, 115, 0, 104, 0, 32, 0, 45, 0, 110, 0, 111, 0, 115, 0, 111, 0, 117, 0, 110, 0, 100, 0, 32, 0, 45, 0, 110, 0, 117, 0, 108, 0, 108, 0, 114, 0, 104, 0, 105, 0, 32, 0, 45, 0, 117, 0, 115, 0, 101, 0, 111, 0, 108, 0, 100, 0, 105, 0, 116, 0, 101, 0, 109, 0, 99, 0, 97, 0, 114, 0, 100, 0, 115, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0
|
||||
]);
|
||||
|
||||
final Uint8List _originalMatchmaking = Uint8List.fromList([
|
||||
63, 0, 69, 0, 110, 0, 99, 0, 114, 0, 121, 0, 112, 0, 116, 0, 105, 0, 111, 0, 110, 0, 84, 0, 111, 0, 107, 0, 101, 0, 110, 0, 61
|
||||
]);
|
||||
|
||||
final Uint8List _patchedMatchmaking = Uint8List.fromList([
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
|
||||
]);
|
||||
|
||||
Future<bool> patchHeadless(File file) async =>
|
||||
_patch(file, _originalHeadless, _patchedHeadless);
|
||||
|
||||
Future<bool> patchMatchmaking(File file) async =>
|
||||
await _patch(file, _originalMatchmaking, _patchedMatchmaking);
|
||||
|
||||
Future<bool> _patch(File file, Uint8List original, Uint8List patched) async {
|
||||
try {
|
||||
if(original.length != patched.length){
|
||||
throw Exception("Cannot mutate length of binary file");
|
||||
}
|
||||
|
||||
var read = await file.readAsBytes();
|
||||
var length = await file.length();
|
||||
var offset = 0;
|
||||
var counter = 0;
|
||||
while(offset < length){
|
||||
if(read[offset] == original[counter]){
|
||||
counter++;
|
||||
}else {
|
||||
counter = 0;
|
||||
}
|
||||
|
||||
offset++;
|
||||
if(counter == original.length){
|
||||
for(var index = 0; index < patched.length; index++){
|
||||
read[offset - counter + index] = patched[index];
|
||||
}
|
||||
|
||||
await file.writeAsBytes(read, mode: FileMode.write);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}catch(_){
|
||||
return false;
|
||||
}
|
||||
}
|
||||
80
common/lib/src/util/path.dart
Normal file
80
common/lib/src/util/path.dart
Normal file
@@ -0,0 +1,80 @@
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
Directory get installationDirectory =>
|
||||
File(Platform.resolvedExecutable).parent;
|
||||
|
||||
Directory get assetsDirectory {
|
||||
var directory = Directory("${installationDirectory.path}\\data\\flutter_assets\\assets");
|
||||
if(directory.existsSync()) {
|
||||
return directory;
|
||||
}
|
||||
|
||||
return installationDirectory;
|
||||
}
|
||||
|
||||
Directory get logsDirectory =>
|
||||
Directory("${installationDirectory.path}\\logs");
|
||||
|
||||
Directory get settingsDirectory =>
|
||||
Directory("${installationDirectory.path}\\settings");
|
||||
|
||||
Directory get tempDirectory =>
|
||||
Directory(Platform.environment["Temp"]!);
|
||||
|
||||
Future<bool> delete(FileSystemEntity file) async {
|
||||
try {
|
||||
await file.delete(recursive: true);
|
||||
return true;
|
||||
}catch(_){
|
||||
return Future.delayed(const Duration(seconds: 5)).then((value) async {
|
||||
try {
|
||||
await file.delete(recursive: true);
|
||||
return true;
|
||||
}catch(_){
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension FortniteVersionExtension on FortniteVersion {
|
||||
static File? findExecutable(Directory directory, String name) {
|
||||
try{
|
||||
var result = directory.listSync(recursive: true)
|
||||
.firstWhere((element) => path.basename(element.path) == name);
|
||||
return File(result.path);
|
||||
}catch(_){
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<File?> get executable async {
|
||||
var result = findExecutable(location, "FortniteClient-Win64-Shipping-Reboot.exe");
|
||||
if(result != null) {
|
||||
return result;
|
||||
}
|
||||
|
||||
var original = findExecutable(location, "FortniteClient-Win64-Shipping.exe");
|
||||
if(original == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var output = File("${original.parent.path}\\FortniteClient-Win64-Shipping-Reboot.exe");
|
||||
await original.copy(output.path);
|
||||
await Future.wait([
|
||||
Isolate.run(() => patchMatchmaking(output)),
|
||||
Isolate.run(() => patchHeadless(output)),
|
||||
]);
|
||||
return output;
|
||||
}
|
||||
|
||||
File? get launcher => findExecutable(location, "FortniteLauncher.exe");
|
||||
|
||||
File? get eacExecutable => findExecutable(location, "FortniteClient-Win64-Shipping_EAC.exe");
|
||||
|
||||
File? get splashBitmap => findExecutable(location, "Splash.bmp");
|
||||
}
|
||||
176
common/lib/src/util/process.dart
Normal file
176
common/lib/src/util/process.dart
Normal file
@@ -0,0 +1,176 @@
|
||||
// ignore_for_file: non_constant_identifier_names
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:ffi';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:ffi/ffi.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');
|
||||
|
||||
Future<void> injectDll(int pid, String dll) async {
|
||||
var process = OpenProcess(
|
||||
0x43A,
|
||||
0,
|
||||
pid
|
||||
);
|
||||
|
||||
var processAddress = GetProcAddress(
|
||||
GetModuleHandle("KERNEL32".toNativeUtf16()),
|
||||
"LoadLibraryA".toNativeUtf8()
|
||||
);
|
||||
|
||||
if (processAddress == nullptr) {
|
||||
throw Exception("Cannot get process address for pid $pid");
|
||||
}
|
||||
|
||||
var dllAddress = VirtualAllocEx(
|
||||
process,
|
||||
nullptr,
|
||||
dll.length + 1,
|
||||
0x3000,
|
||||
0x4
|
||||
);
|
||||
|
||||
var writeMemoryResult = WriteProcessMemory(
|
||||
process,
|
||||
dllAddress,
|
||||
dll.toNativeUtf8(),
|
||||
dll.length,
|
||||
nullptr
|
||||
);
|
||||
|
||||
if (writeMemoryResult != 1) {
|
||||
throw Exception("Memory write failed");
|
||||
}
|
||||
|
||||
var createThreadResult = _CreateRemoteThread(
|
||||
process,
|
||||
nullptr,
|
||||
0,
|
||||
processAddress,
|
||||
dllAddress,
|
||||
0,
|
||||
nullptr
|
||||
);
|
||||
|
||||
if (createThreadResult == -1) {
|
||||
throw Exception("Thread creation failed");
|
||||
}
|
||||
|
||||
var closeResult = CloseHandle(process);
|
||||
if(closeResult != 1){
|
||||
throw Exception("Cannot close handle");
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> runElevatedProcess(String executable, String args) async {
|
||||
var 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>();
|
||||
var shellResult = ShellExecuteEx(shellInput);
|
||||
return shellResult == 1;
|
||||
}
|
||||
|
||||
|
||||
int startBackgroundProcess(String executable, List<String> args) {
|
||||
var executablePath = TEXT('$executable ${args.map((entry) => '"$entry"').join(" ")}');
|
||||
var startupInfo = calloc<STARTUPINFO>();
|
||||
var processInfo = calloc<PROCESS_INFORMATION>();
|
||||
var success = CreateProcess(
|
||||
nullptr,
|
||||
executablePath,
|
||||
nullptr,
|
||||
nullptr,
|
||||
FALSE,
|
||||
CREATE_NO_WINDOW,
|
||||
nullptr,
|
||||
nullptr,
|
||||
startupInfo,
|
||||
processInfo
|
||||
);
|
||||
if (success == 0) {
|
||||
var error = GetLastError();
|
||||
throw Exception("Cannot start process: $error");
|
||||
}
|
||||
|
||||
var pid = processInfo.ref.dwProcessId;
|
||||
free(startupInfo);
|
||||
free(processInfo);
|
||||
return pid;
|
||||
}
|
||||
|
||||
int _NtResumeProcess(int hWnd) {
|
||||
final function = _ntdll.lookupFunction<Int32 Function(IntPtr hWnd),
|
||||
int Function(int hWnd)>('NtResumeProcess');
|
||||
return function(hWnd);
|
||||
}
|
||||
|
||||
int _NtSuspendProcess(int hWnd) {
|
||||
final function = _ntdll.lookupFunction<Int32 Function(IntPtr hWnd),
|
||||
int Function(int hWnd)>('NtSuspendProcess');
|
||||
return function(hWnd);
|
||||
}
|
||||
|
||||
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);
|
||||
WaitForSingleObject(processHandle, INFINITE);
|
||||
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 completer.future;
|
||||
}
|
||||
92
common/lib/src/util/reboot.dart
Normal file
92
common/lib/src/util/reboot.dart
Normal file
@@ -0,0 +1,92 @@
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:archive/archive_io.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:reboot_common/common.dart';
|
||||
|
||||
const String rebootDownloadUrl =
|
||||
"https://nightly.link/Milxnor/Project-Reboot-3.0/workflows/msbuild/main/Release.zip";
|
||||
final File rebootDllFile = File("${assetsDirectory.path}\\dlls\\reboot.dll");
|
||||
|
||||
List<String> createRebootArgs(String username, String password, bool host, String additionalArgs) {
|
||||
if(password.isEmpty) {
|
||||
username = username.isEmpty ? kDefaultPlayerName : username;
|
||||
username = host ? "$username${Random().nextInt(1000)}" : username;
|
||||
username = '$username@projectreboot.dev';
|
||||
}
|
||||
password = password.isNotEmpty ? password : "Rebooted";
|
||||
var 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(host){
|
||||
args.addAll([
|
||||
"-nullrhi",
|
||||
"-nosplash",
|
||||
"-nosound",
|
||||
]);
|
||||
}
|
||||
|
||||
if(additionalArgs.isNotEmpty){
|
||||
args.addAll(additionalArgs.split(" "));
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
|
||||
Future<int> downloadRebootDll(String url, int? lastUpdateMs) async {
|
||||
Directory? outputDir;
|
||||
var now = DateTime.now();
|
||||
try {
|
||||
var lastUpdate = await _getLastUpdate(lastUpdateMs);
|
||||
var exists = await rebootDllFile.exists();
|
||||
if (lastUpdate != null && now.difference(lastUpdate).inHours <= 24 && exists) {
|
||||
return lastUpdateMs!;
|
||||
}
|
||||
|
||||
var response = await http.get(Uri.parse(rebootDownloadUrl));
|
||||
outputDir = await installationDirectory.createTemp("reboot_out");
|
||||
var tempZip = File("${outputDir.path}\\reboot.zip");
|
||||
await tempZip.writeAsBytes(response.bodyBytes);
|
||||
await extractFileToDisk(tempZip.path, outputDir.path);
|
||||
var rebootDll = File(outputDir.listSync().firstWhere((element) => path.extension(element.path) == ".dll").path);
|
||||
if (!exists || sha1.convert(await rebootDllFile.readAsBytes()) != sha1.convert(await rebootDll.readAsBytes())) {
|
||||
await rebootDllFile.writeAsBytes(await rebootDll.readAsBytes());
|
||||
}
|
||||
|
||||
return now.millisecondsSinceEpoch;
|
||||
}catch(message) {
|
||||
if(url == rebootDownloadUrl){
|
||||
var asset = File('${assetsDirectory.path}\\dlls\\reboot.dll');
|
||||
await rebootDllFile.writeAsBytes(asset.readAsBytesSync());
|
||||
return now.millisecondsSinceEpoch;
|
||||
}
|
||||
|
||||
throw Exception("Cannot download reboot.zip, invalid zip: $message");
|
||||
}finally{
|
||||
if(outputDir != null) {
|
||||
delete(outputDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<DateTime?> _getLastUpdate(int? lastUpdateMs) async {
|
||||
return lastUpdateMs != null
|
||||
? DateTime.fromMillisecondsSinceEpoch(lastUpdateMs)
|
||||
: null;
|
||||
}
|
||||
Reference in New Issue
Block a user