mirror of
https://github.com/Auties00/Reboot-Launcher.git
synced 2026-01-13 19:22:22 +01:00
update
This commit is contained in:
333
lib/src/embedded/auth.dart
Normal file
333
lib/src/embedded/auth.dart
Normal file
@@ -0,0 +1,333 @@
|
||||
import 'dart:collection';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:jaguar/http/context/context.dart';
|
||||
import 'package:reboot_launcher/src/embedded/utils.dart';
|
||||
|
||||
import '../util/os.dart';
|
||||
|
||||
final Directory _profiles = Directory("${Platform.environment["UserProfile"]}\\.reboot_launcher\\backend\\profiles");
|
||||
|
||||
const String _token = "reboot_token";
|
||||
const String _clientId = "reboot_client";
|
||||
const String _device = "reboot_device";
|
||||
const String _sessionId = "3c3662bcb661d6de679c636744c66b62";
|
||||
|
||||
List<Map<String, Object>> getAccounts(Context context) {
|
||||
return context.query.getList("accountId").map(getAccount).toList();
|
||||
}
|
||||
|
||||
Map<String, Object> getAccount(String account) {
|
||||
return {"id": account, "displayName": _parseUsername(account), "externalAuths": {}};
|
||||
}
|
||||
|
||||
Map<String, Object> getAccountInfo(Context context) {
|
||||
var usernameId = context.pathParams.get("accountId")!;
|
||||
var accountName = _parseUsername(usernameId);
|
||||
return {
|
||||
"id": usernameId,
|
||||
"displayName": accountName,
|
||||
"name": "Reboot",
|
||||
"email": usernameId,
|
||||
"failedLoginAttempts": 0,
|
||||
"lastLogin": "2022-11-08T18:55:52.341Z",
|
||||
"numberOfDisplayNameChanges": 0,
|
||||
"ageGroup": "UNKNOWN",
|
||||
"headless": false,
|
||||
"country": "US",
|
||||
"lastName": "Server",
|
||||
"preferredLanguage": "en",
|
||||
"canUpdateDisplayName": false,
|
||||
"tfaEnabled": false,
|
||||
"emailVerified": true,
|
||||
"minorVerified": false,
|
||||
"minorExpected": false,
|
||||
"minorStatus": "UNKNOWN"
|
||||
};
|
||||
}
|
||||
|
||||
List<Map<String, Object>> getExternalAuths(Context context) => [];
|
||||
|
||||
Future<Map<String, Object>> getOAuthToken(Context context) async {
|
||||
var usernameId = await _getUsername(context);
|
||||
var accountName = _parseUsername(usernameId);
|
||||
return {
|
||||
"access_token": _token,
|
||||
"expires_in": 28800,
|
||||
"expires_at": "9999-12-02T01:12:01.100Z",
|
||||
"token_type": "bearer",
|
||||
"refresh_token": _token,
|
||||
"refresh_expires": 86400,
|
||||
"refresh_expires_at": "9999-12-02T01:12:01.100Z",
|
||||
"account_id": usernameId,
|
||||
"client_id": _clientId,
|
||||
"internal_client": true,
|
||||
"client_service": "fortnite",
|
||||
"displayName": accountName,
|
||||
"app": "fortnite",
|
||||
"in_app_id": usernameId,
|
||||
"device_id": _device
|
||||
};
|
||||
}
|
||||
|
||||
Future<String> _getUsername(Context context) async {
|
||||
var params = await parseBody(context);
|
||||
var username = params["username"];
|
||||
return username ?? "unknown@projectreboot.dev";
|
||||
}
|
||||
|
||||
Map<String, Object> verifyOAuthToken(Context context) {
|
||||
return {
|
||||
"token": _token,
|
||||
"session_id": _sessionId,
|
||||
"token_type": "bearer",
|
||||
"client_id": _clientId,
|
||||
"internal_client": true,
|
||||
"client_service": "fortnite",
|
||||
"account_id": "unknown",
|
||||
"expires_in": 28800,
|
||||
"expires_at": "9999-12-02T01:12:01.100Z",
|
||||
"auth_method": "exchange_code",
|
||||
"display_name": "unknown",
|
||||
"app": "fortnite",
|
||||
"in_app_id": "unknown",
|
||||
"device_id": _device
|
||||
};
|
||||
}
|
||||
|
||||
List<Map<String, Object>> getExchange(Context context) => [];
|
||||
|
||||
List<String> getSsoDomains(Context context) => [
|
||||
"unrealengine.com",
|
||||
"unrealtournament.com",
|
||||
"fortnite.com",
|
||||
"epicgames.com"
|
||||
];
|
||||
|
||||
String tryPlayOnPlatform(Context context) => "true";
|
||||
|
||||
List<Map<String, Object>> getFeatures(Context context) => [];
|
||||
|
||||
Map<String, Object?> getProfile(Context context){
|
||||
var profileId = context.query.get("profileId");
|
||||
if (profileId == null) {
|
||||
return {"Error": "Profile not defined."};
|
||||
}
|
||||
|
||||
var profileJson = _getProfileJson(profileId, context);
|
||||
var profileFile = _getProfileFile(context);
|
||||
var baseRevision = profileJson["rvn"] ?? 0;
|
||||
var queryRevision = context.query.getInt("rvn") ?? -1;
|
||||
var profileChanges = _getFullProfileUpdate(context, profileId, profileJson, queryRevision, baseRevision);
|
||||
if(profileId == "athena" && !profileFile.existsSync()) {
|
||||
profileFile.writeAsStringSync(json.encode(profileJson), flush: true);
|
||||
}
|
||||
|
||||
return {
|
||||
"profileRevision": baseRevision,
|
||||
"profileId": profileId,
|
||||
"profileChangesBaseRevision": baseRevision,
|
||||
"profileChanges": profileChanges,
|
||||
"profileCommandRevision": profileJson["commandRevision"] ?? 0,
|
||||
"serverTime": "2022-11-08T18:55:52.341Z",
|
||||
"responseVersion": 1
|
||||
};
|
||||
}
|
||||
|
||||
Map<String, dynamic> _getProfileJson(String profileId, Context context) {
|
||||
if(profileId == "athena"){
|
||||
var profile = _getProfileFile(context);
|
||||
if(profile.existsSync()){
|
||||
return json.decode(profile.readAsStringSync());
|
||||
}
|
||||
|
||||
var body = loadEmbedded("profiles/$profileId.json").readAsStringSync();
|
||||
return json.decode(body);
|
||||
}
|
||||
|
||||
var profileJson = json.decode(loadEmbedded("profiles/$profileId.json").readAsStringSync());
|
||||
return profileJson;
|
||||
}
|
||||
|
||||
Future<Map<String, Object>> equipItem(Context context) async {
|
||||
var profileFile = _getProfileFile(context);
|
||||
var profileJson = json.decode(profileFile.readAsStringSync());
|
||||
var baseRevision = profileJson["rvn"] ?? 0;
|
||||
var queryRevision = context.query.getInt("rvn") ?? -1;
|
||||
|
||||
var body = json.decode(utf8.decode(await context.body));
|
||||
var variant = _getReturnVariant(body, profileJson);
|
||||
var change = _getStatsChanged(body, profileJson);
|
||||
var profileChanges = _getProfileChanges(queryRevision, baseRevision, profileJson, change, body, variant);
|
||||
profileFile.writeAsStringSync(json.encode(profileJson));
|
||||
return {
|
||||
"profileRevision": baseRevision,
|
||||
"profileId": "athena",
|
||||
"profileChangesBaseRevision": baseRevision,
|
||||
"profileChanges": profileChanges,
|
||||
"profileCommandRevision": profileJson["commandRevision"] ?? 0,
|
||||
"serverTime": "2022-11-08T18:55:52.341Z",
|
||||
"responseVersion": 1
|
||||
};
|
||||
}
|
||||
|
||||
List<dynamic> _getProfileChanges(int queryRevision, baseRevision, profileJson, bool change, body, bool variant) {
|
||||
var changes = [];
|
||||
if (change) {
|
||||
var category = ("favorite_${body["slotName"] ?? "character"}")
|
||||
.toLowerCase();
|
||||
if (category == "favorite_itemwrap") {
|
||||
category += "s";
|
||||
}
|
||||
|
||||
profileJson["rvn"] = (profileJson["rvn"] ?? 0) + 1;
|
||||
profileJson["commandRevision"] = (profileJson["commandRevision"] ?? 0) + 1;
|
||||
|
||||
changes.add({
|
||||
"changeType": "statModified",
|
||||
"name": category,
|
||||
"value": profileJson["stats"]["attributes"][category]
|
||||
});
|
||||
if (variant) {
|
||||
changes.add({
|
||||
"changeType": "itemAttrChanged",
|
||||
"itemId": body["itemToSlot"],
|
||||
"attributeName": "variants",
|
||||
"attributeValue": profileJson["items"][body["itemToSlot"]]["attributes"]["variants"]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if(queryRevision != baseRevision){
|
||||
return [{
|
||||
"changeType": "fullProfileUpdate",
|
||||
"profile": profileJson
|
||||
}];
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
bool _getStatsChanged(body, profileJson) {
|
||||
var slotName = body["slotName"];
|
||||
if (slotName == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (slotName) {
|
||||
case "Character":
|
||||
profileJson["stats"]["attributes"]["favorite_character"] =
|
||||
body["itemToSlot"] ?? "";
|
||||
return true;
|
||||
case "Backpack":
|
||||
profileJson["stats"]["attributes"]["favorite_backpack"] =
|
||||
body["itemToSlot"] ?? "";
|
||||
return true;
|
||||
case "Pickaxe":
|
||||
profileJson["stats"]["attributes"]["favorite_pickaxe"] =
|
||||
body["itemToSlot"] ?? "";
|
||||
return true;
|
||||
case "Glider":
|
||||
profileJson["stats"]["attributes"]["favorite_glider"] =
|
||||
body["itemToSlot"] ?? "";
|
||||
return true;
|
||||
case "SkyDiveContrail":
|
||||
profileJson["stats"]["attributes"]["favorite_skydivecontrail"] =
|
||||
body["itemToSlot"] ?? "";
|
||||
return true;
|
||||
case "MusicPack":
|
||||
profileJson["stats"]["attributes"]["favorite_musicpack"] =
|
||||
body["itemToSlot"] ?? "";
|
||||
return true;
|
||||
case "LoadingScreen":
|
||||
profileJson["stats"]["attributes"]["favorite_loadingscreen"] =
|
||||
body["itemToSlot"] ?? "";
|
||||
return true;
|
||||
case "Dance":
|
||||
var index = body["indexWithinSlot"] ?? 0;
|
||||
if (index >= 0) {
|
||||
profileJson["stats"]["attributes"]["favorite_dance"][index] =
|
||||
body["itemToSlot"] ?? "";
|
||||
}
|
||||
|
||||
return true;
|
||||
case "ItemWrap":
|
||||
var index = body["indexWithinSlot"] ?? 0;
|
||||
if (index < 0) {
|
||||
for (var i = 0; i < 7; i++) {
|
||||
profileJson["stats"]["attributes"]["favorite_itemwraps"][i] =
|
||||
body["itemToSlot"] ?? "";
|
||||
}
|
||||
} else {
|
||||
profileJson["stats"]["attributes"]["favorite_itemwraps"][index] =
|
||||
body["itemToSlot"] ?? "";
|
||||
}
|
||||
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool _getReturnVariant(body, profileJson) {
|
||||
var variantUpdates = body["variantUpdates"] ?? [];
|
||||
if(!variantUpdates.toString().contains("active")){
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
var variantJson = profileJson["items"][body["itemToSlot"]]["attributes"]["variants"] ?? [];
|
||||
if (variantJson.isEmpty) {
|
||||
variantJson = variantUpdates;
|
||||
}
|
||||
|
||||
for (var i in variantJson) {
|
||||
try {
|
||||
if (variantJson[i]["channel"].toLowerCase() == body["variantUpdates"][i]["channel"].toLowerCase()) {
|
||||
profileJson["items"][body["itemToSlot"]]["attributes"]["variants"][i]["active"] = body["variantUpdates"][i]["active"] ?? "";
|
||||
}
|
||||
} catch (_) {
|
||||
// Ignored
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (_) {
|
||||
// Ignored
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
List<Map<String, Object?>> _getFullProfileUpdate(Context context, String profileName, Map<String, dynamic> profileJson, int queryRevision, int baseRevision) {
|
||||
if (queryRevision == baseRevision) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (profileName == "athena") {
|
||||
var season = parseSeason(context);
|
||||
profileJson["stats"]["attributes"]["season_num"] = season;
|
||||
profileJson["stats"]["attributes"]["book_purchased"] = true;
|
||||
profileJson["stats"]["attributes"]["book_level"] = 100;
|
||||
profileJson["stats"]["attributes"]["season_match_boost"] = 100;
|
||||
profileJson["stats"]["attributes"]["season_friend_match_boost"] = 100;
|
||||
}
|
||||
|
||||
return [{
|
||||
"changeType": "fullProfileUpdate",
|
||||
"profile": profileJson
|
||||
}];
|
||||
}
|
||||
|
||||
String _parseUsername(String username) =>
|
||||
username.contains("@") ? username.split("@")[0] : username;
|
||||
|
||||
File _getProfileFile(Context context) {
|
||||
if(!_profiles.existsSync()){
|
||||
_profiles.createSync(recursive: true);
|
||||
}
|
||||
|
||||
return File("${_profiles.path}\\ClientProfile-${parseSeasonBuild(context)}.json");
|
||||
}
|
||||
|
||||
36
lib/src/embedded/error.dart
Normal file
36
lib/src/embedded/error.dart
Normal file
@@ -0,0 +1,36 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:jaguar/jaguar.dart';
|
||||
|
||||
class EmbeddedErrorWriter extends ErrorWriter {
|
||||
static const String _errorName = "errors.com.lawinserver.common.not_found";
|
||||
static const String _errorCode = "1004";
|
||||
|
||||
@override
|
||||
FutureOr<Response> make404(Context ctx) {
|
||||
stdout.writeln("Unknown path: ${ctx.uri} with method ${ctx.method}");
|
||||
ctx.response.headers.set('X-Epic-Error-Name', _errorName);
|
||||
ctx.response.headers.set('X-Epic-Error-Code', _errorCode);
|
||||
return Response.json(
|
||||
statusCode: 204,
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<Response> make500(Context ctx, Object error, [StackTrace? stack]) {
|
||||
ctx.response.headers.set('X-Epic-Error-Name', _errorName);
|
||||
ctx.response.headers.set('X-Epic-Error-Code', _errorCode);
|
||||
return Response(
|
||||
statusCode: 500,
|
||||
body: {
|
||||
"errorCode": _errorName,
|
||||
"errorMessage": "Sorry the resource you were trying to find could not be found",
|
||||
"numericErrorCode": _errorCode,
|
||||
"originatingService": "any",
|
||||
"intent": "prod"
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
33
lib/src/embedded/lightswitch.dart
Normal file
33
lib/src/embedded/lightswitch.dart
Normal file
@@ -0,0 +1,33 @@
|
||||
import 'package:jaguar/http/context/context.dart';
|
||||
|
||||
Map<String, Object?> getFortniteStatus(Context context) => {
|
||||
"serviceInstanceId": "fortnite",
|
||||
"status": "UP",
|
||||
"message": "Fortnite is online",
|
||||
"maintenanceUri": null,
|
||||
"overrideCatalogIds": ["a7f138b2e51945ffbfdacc1af0541053"],
|
||||
"allowedActions": [],
|
||||
"banned": false,
|
||||
"launcherInfoDTO": {
|
||||
"appName": "Fortnite",
|
||||
"catalogItemId": "4fe75bbc5a674f4f9b356b5c90567da5",
|
||||
"namespace": "fn"
|
||||
}
|
||||
};
|
||||
|
||||
List<Map<String, Object?>> getBulkStatus(Context context) => [
|
||||
{
|
||||
"serviceInstanceId": "fortnite",
|
||||
"status": "UP",
|
||||
"message": "fortnite is up.",
|
||||
"maintenanceUri": null,
|
||||
"overrideCatalogIds": ["a7f138b2e51945ffbfdacc1af0541053"],
|
||||
"allowedActions": ["PLAY", "DOWNLOAD"],
|
||||
"banned": false,
|
||||
"launcherInfoDTO": {
|
||||
"appName": "Fortnite",
|
||||
"catalogItemId": "4fe75bbc5a674f4f9b356b5c90567da5",
|
||||
"namespace": "fn"
|
||||
}
|
||||
}
|
||||
];
|
||||
144
lib/src/embedded/matchmaking.dart
Normal file
144
lib/src/embedded/matchmaking.dart
Normal file
@@ -0,0 +1,144 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:jaguar/http/context/context.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import 'package:jaguar/jaguar.dart';
|
||||
|
||||
String _build = "0";
|
||||
String? _customIp;
|
||||
|
||||
Map<String, Object> getPlayerTicket(Context context){
|
||||
var bucketId = context.query.get("bucketId");
|
||||
if(bucketId == null){
|
||||
return {"Error": "Missing bucket id"};
|
||||
}
|
||||
|
||||
_build = bucketId.split(":")[0];
|
||||
_customIp = context.query.get("player.option.customKey");
|
||||
return {
|
||||
"serviceUrl": "ws://127.0.0.1:8080",
|
||||
"ticketType": "mms-player",
|
||||
"payload": "69=",
|
||||
"signature": "420="
|
||||
};
|
||||
}
|
||||
|
||||
Map<String, Object?> getSessionAccount(Context context) => {
|
||||
"accountId": context.pathParams.get("accountId"),
|
||||
"sessionId": context.pathParams.get("sessionId"),
|
||||
"key": "AOJEv8uTFmUh7XM2328kq9rlAzeQ5xzWzPIiyKn2s7s="
|
||||
};
|
||||
|
||||
Future<Map<String, Object?>> getMatch(Context context, String Function() ipQuery) async {
|
||||
var ipAndPort = _customIp ?? ipQuery().trim();
|
||||
var ip = ipAndPort.contains(":") ? ipAndPort.split(":")[0] : ipAndPort;
|
||||
var port = ipAndPort.contains(":") ? int.parse(ipAndPort.split(":")[1]) : 7777;
|
||||
return {
|
||||
"id": context.pathParams.get("sessionId"),
|
||||
"ownerId": _randomUUID(),
|
||||
"ownerName": "[DS]fortnite-liveeugcec1c2e30ubrcore0a-z8hj-1968",
|
||||
"serverName": "[DS]fortnite-liveeugcec1c2e30ubrcore0a-z8hj-1968",
|
||||
"serverAddress": ip,
|
||||
"serverPort": port,
|
||||
"maxPublicPlayers": 220,
|
||||
"openPublicPlayers": 175,
|
||||
"maxPrivatePlayers": 0,
|
||||
"openPrivatePlayers": 0,
|
||||
"attributes": {
|
||||
"REGION_s": "EU",
|
||||
"GAMEMODE_s": "FORTATHENA",
|
||||
"ALLOWBROADCASTING_b": true,
|
||||
"SUBREGION_s": "GB",
|
||||
"DCID_s": "FORTNITE-LIVEEUGCEC1C2E30UBRCORE0A-14840880",
|
||||
"tenant_s": "Fortnite",
|
||||
"MATCHMAKINGPOOL_s": "Any",
|
||||
"STORMSHIELDDEFENSETYPE_i": 0,
|
||||
"HOTFIXVERSION_i": 0,
|
||||
"PLAYLISTNAME_s": "Playlist_DefaultSolo",
|
||||
"SESSIONKEY_s": _randomUUID(),
|
||||
"TENANT_s": "Fortnite",
|
||||
"BEACONPORT_i": 15009
|
||||
},
|
||||
"publicPlayers": [],
|
||||
"privatePlayers": [],
|
||||
"totalPlayers": 45,
|
||||
"allowJoinInProgress": false,
|
||||
"shouldAdvertise": false,
|
||||
"isDedicated": false,
|
||||
"usesStats": false,
|
||||
"allowInvites": false,
|
||||
"usesPresence": false,
|
||||
"allowJoinViaPresence": true,
|
||||
"allowJoinViaPresenceFriendsOnly": false,
|
||||
"buildUniqueId": _build,
|
||||
"lastUpdated": "2022-11-08T18:55:52.341Z",
|
||||
"started": false
|
||||
};
|
||||
}
|
||||
|
||||
List<Map<String, Object>> getMatchmakingRequests() => [];
|
||||
|
||||
void queueMatchmaking(WebSocket ws) {
|
||||
var now = DateTime.now();
|
||||
var ticketId = md5.convert(utf8.encode("1$now")).toString();
|
||||
var matchId = md5.convert(utf8.encode("2$now")).toString();
|
||||
var sessionId = md5.convert(utf8.encode("3$now")).toString();
|
||||
|
||||
ws.addUtf8Text(utf8.encode(
|
||||
jsonEncode({
|
||||
"payload": {
|
||||
"state": "Connecting"
|
||||
},
|
||||
"name": "StatusUpdate"
|
||||
})
|
||||
));
|
||||
|
||||
ws.addUtf8Text(utf8.encode(
|
||||
jsonEncode({
|
||||
"payload": {
|
||||
"totalPlayers": 1,
|
||||
"connectedPlayers": 1,
|
||||
"state": "Waiting"
|
||||
},
|
||||
"name": "StatusUpdate"
|
||||
})
|
||||
));
|
||||
|
||||
ws.addUtf8Text(utf8.encode(
|
||||
jsonEncode({
|
||||
"payload": {
|
||||
"ticketId": ticketId,
|
||||
"queuedPlayers": 0,
|
||||
"estimatedWaitSec": 0,
|
||||
"status": {},
|
||||
"state": "Queued"
|
||||
},
|
||||
"name": "StatusUpdate"
|
||||
})
|
||||
));
|
||||
|
||||
ws.addUtf8Text(utf8.encode(
|
||||
jsonEncode({
|
||||
"payload": {
|
||||
"matchId": matchId,
|
||||
"state": "SessionAssignment"
|
||||
},
|
||||
"name": "StatusUpdate"
|
||||
})
|
||||
));
|
||||
|
||||
ws.addUtf8Text(utf8.encode(
|
||||
jsonEncode({
|
||||
"payload": {
|
||||
"matchId": matchId,
|
||||
"sessionId": sessionId,
|
||||
"joinDelaySec": 1
|
||||
},
|
||||
"name": "Play"
|
||||
})
|
||||
));
|
||||
}
|
||||
|
||||
String _randomUUID() => const Uuid().v4().replaceAll("-", "").toUpperCase();
|
||||
1205
lib/src/embedded/misc.dart
Normal file
1205
lib/src/embedded/misc.dart
Normal file
File diff suppressed because it is too large
Load Diff
16
lib/src/embedded/privacy.dart
Normal file
16
lib/src/embedded/privacy.dart
Normal file
@@ -0,0 +1,16 @@
|
||||
import 'package:jaguar/http/context/context.dart';
|
||||
import 'package:reboot_launcher/src/embedded/utils.dart';
|
||||
|
||||
Map<String, Object?> getPrivacy(Context context) => {
|
||||
"accountId": context.pathParams.get("accountId"),
|
||||
"optOutOfPublicLeaderboards": false
|
||||
};
|
||||
|
||||
|
||||
Future<Map<String, Object?>> postPrivacy(Context context) async {
|
||||
var body = await parseBody(context);
|
||||
return {
|
||||
"accountId": context.pathParams.get("accountId"),
|
||||
"optOutOfPublicLeaderboards": body["optOutOfPublicLeaderboards"]
|
||||
};
|
||||
}
|
||||
138
lib/src/embedded/server.dart
Normal file
138
lib/src/embedded/server.dart
Normal file
@@ -0,0 +1,138 @@
|
||||
import "dart:async";
|
||||
import "dart:io";
|
||||
|
||||
import "package:jaguar/jaguar.dart";
|
||||
import "package:reboot_launcher/src/embedded/auth.dart";
|
||||
import 'package:reboot_launcher/src/embedded/misc.dart';
|
||||
import 'package:reboot_launcher/src/embedded/privacy.dart';
|
||||
import "package:reboot_launcher/src/embedded/storage.dart";
|
||||
import 'package:reboot_launcher/src/embedded/storefront.dart';
|
||||
import "package:reboot_launcher/src/embedded/version.dart";
|
||||
|
||||
import '../util/server.dart';
|
||||
import "error.dart";
|
||||
import "lightswitch.dart";
|
||||
import 'matchmaking.dart';
|
||||
|
||||
bool _loggingCapabilities = false;
|
||||
|
||||
Future<Jaguar> startEmbeddedServer(String Function() ipQuery) async {
|
||||
var server = _createServer(ipQuery);
|
||||
await server.serve(logRequests: true);
|
||||
return server;
|
||||
}
|
||||
|
||||
Future<Jaguar> startEmbeddedMatchmaker() async {
|
||||
var server = _createMatchmaker();
|
||||
server.serve(logRequests: true);
|
||||
return server;
|
||||
}
|
||||
|
||||
Jaguar _createServer(String Function() ipQuery) {
|
||||
var server = Jaguar(address: "127.0.0.1", port: 3551, errorWriter: EmbeddedErrorWriter());
|
||||
|
||||
// Version
|
||||
server.getJson("/fortnite/api/version", getVersion);
|
||||
server.getJson("/fortnite/api/v2/versioncheck/*", hasUpdate);
|
||||
server.getJson("/fortnite/api/v2/versioncheck*", hasUpdate);
|
||||
server.getJson("/fortnite/api/versioncheck*", hasUpdate);
|
||||
|
||||
// Auth
|
||||
server.getJson("/account/api/public/account/displayName/:accountId", getAccountInfo);
|
||||
server.getJson("/account/api/public/account/:accountId", getAccountInfo);
|
||||
server.getJson("/account/api/public/account/:accountId/externalAuths", getExternalAuths);
|
||||
server.getJson("/account/api/public/account", getAccounts);
|
||||
server.delete("/account/api/oauth/sessions/kill/*", (context) => Response(statusCode: 204));
|
||||
server.getJson("/account/api/oauth/verify", verifyOAuthToken);
|
||||
server.postJson("/account/api/oauth/token", getOAuthToken);
|
||||
server.postJson("/account/api/oauth/exchange", getExchange);
|
||||
server.getJson("/account/api/epicdomains/ssodomains", getSsoDomains);
|
||||
server.post("/fortnite/api/game/v2/tryPlayOnPlatform/account/*", tryPlayOnPlatform);
|
||||
server.post("/datarouter/api/v1/public/data/*", (context) => Response(statusCode: 204));
|
||||
server.getJson("/fortnite/api/game/v2/enabled_features", getFeatures);
|
||||
server.postJson("/fortnite/api/game/v2/grant_access/*", (context) => Response(statusCode: 204));
|
||||
server.postJson("/fortnite/api/game/v2/profile/:profileId/client/EquipBattleRoyaleCustomization", equipItem);
|
||||
server.postJson("/fortnite/api/game/v2/profile/:profileId/client/*", getProfile);
|
||||
|
||||
// Storage
|
||||
server.getJson("/fortnite/api/cloudstorage/system", getStorageSettings);
|
||||
server.get("/fortnite/api/cloudstorage/system/:file", getStorageSetting);
|
||||
server.getJson("/fortnite/api/cloudstorage/user/:accountId", getStorageAccount);
|
||||
server.getJson("/fortnite/api/cloudstorage/user/:accountId/:file", getStorageFile);
|
||||
server.put("/fortnite/api/cloudstorage/user/:accountId/:file", addStorageFile);
|
||||
|
||||
// Status
|
||||
server.getJson("/lightswitch/api/service/Fortnite/status", getFortniteStatus);
|
||||
server.getJson("/lightswitch/api/service/bulk/status", getBulkStatus);
|
||||
|
||||
// Keychain and catalog
|
||||
server.get("/fortnite/api/storefront/v2/catalog", getCatalog);
|
||||
server.get("/fortnite/api/storefront/v2/keychain", getKeyChain);
|
||||
server.get("/catalog/api/shared/bulk/offers", getOffers);
|
||||
|
||||
// Matchmaking
|
||||
server.get("/fortnite/api/matchmaking/session/findPlayer/*", (context) => Response(statusCode: 200));
|
||||
server.getJson("/fortnite/api/game/v2/matchmakingservice/ticket/player/*", getPlayerTicket);
|
||||
server.getJson("/fortnite/api/game/v2/matchmaking/account/:accountId/session/:sessionId", getSessionAccount);
|
||||
server.getJson("/fortnite/api/matchmaking/session/:sessionId", (context) => getMatch(context, ipQuery));
|
||||
server.post("/fortnite/api/matchmaking/session/:accountId/join", (context) => Response(statusCode: 204));
|
||||
server.postJson("/fortnite/api/matchmaking/session/matchMakingRequest", (context) => getMatchmakingRequests);
|
||||
|
||||
// Misc
|
||||
server.getJson("/api/v1/events/Fortnite/download/*", getDownload);
|
||||
server.getJson("/fortnite/api/receipts/v1/account/:accountId/receipts", getReceipts);
|
||||
server.getJson("/content/api/pages/*", getContentPages);
|
||||
server.getJson("/friends/api/v1/:accountId/settings", getFriendsSettings);
|
||||
server.getJson("/friends/api/v1/:accountId/blocklist", getFriendsBlocklist);
|
||||
server.getJson("/friends/api/public/blocklist/:accountId", getFriendsBlocklist);
|
||||
server.getJson("/friends/api/public/friends/:accountId", getFriendsList);
|
||||
server.getJson("/friends/api/public/list/fortnite/:accountId/recentPlayers", getRecentPlayers);
|
||||
server.getJson("/fortnite/api/calendar/v1/timeline", getTimeline);
|
||||
server.getJson("/fortnite/api/game/v2/events/tournamentandhistory/:accountId/EU/WindowsClient", getTournamentHistory);
|
||||
server.get("/waitingroom/api/waitingroom", (context) => Response(statusCode: 204));
|
||||
server.postJson("/api/v1/user/setting", (context) => []);
|
||||
server.getJson("/eulatracking/api/public/agreements/fn/account/*", (context) => Response(statusCode: 204));
|
||||
server.getJson("/socialban/api/public/v1/:accountId", getSocialBan);
|
||||
server.getJson("/party/api/v1/Fortnite/user/*", getParty);
|
||||
server.getJson("/friends/api/v1/*/settings", (context) => {});
|
||||
server.getJson("/friends/api/v1/*/blocklist", (context) => {});
|
||||
server.getJson("/friends/api/public/friends", (context) => []);
|
||||
server.getJson("/friends/api/v1/:accountId/summary", (context) => []);
|
||||
server.getJson("/friends/api/public/list/fortnite/*/recentPlayers", (context) => []);
|
||||
server.getJson("/friends/api/public/blocklist/*", getBlockedFriends);
|
||||
|
||||
// Privacy
|
||||
server.getJson("/fortnite/api/game/v2/privacy/account/:accountId", getPrivacy);
|
||||
server.postJson("/fortnite/api/game/v2/privacy/account/:accountId", postPrivacy);
|
||||
|
||||
return _addLoggingCapabilities(server);
|
||||
}
|
||||
Jaguar _createMatchmaker(){
|
||||
var server = Jaguar(address: "127.0.0.1", port: 8080);
|
||||
WebSocket? ws;
|
||||
server.wsStream(
|
||||
"/",
|
||||
(_, input) => ws = input,
|
||||
after: [(_) => queueMatchmaking(ws!)]
|
||||
);
|
||||
return _addLoggingCapabilities(server);
|
||||
}
|
||||
|
||||
Jaguar _addLoggingCapabilities(Jaguar server) {
|
||||
if(_loggingCapabilities){
|
||||
return server;
|
||||
}
|
||||
|
||||
server.log.onRecord.listen((line) {
|
||||
stdout.writeln(line);
|
||||
serverLogFile.writeAsString("$line\n", mode: FileMode.append);
|
||||
});
|
||||
|
||||
server.onException.add((ctx, exception, trace) {
|
||||
stderr.writeln("An error occurred: $exception");
|
||||
serverLogFile.writeAsString("An error occurred at ${ctx.uri}: \n$exception\n$trace\n", mode: FileMode.append);
|
||||
});
|
||||
|
||||
_loggingCapabilities = true;
|
||||
return server;
|
||||
}
|
||||
111
lib/src/embedded/storage.dart
Normal file
111
lib/src/embedded/storage.dart
Normal file
@@ -0,0 +1,111 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:jaguar/jaguar.dart';
|
||||
import 'package:jaguar/http/context/context.dart';
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:reboot_launcher/src/embedded/utils.dart';
|
||||
|
||||
import '../util/os.dart';
|
||||
|
||||
final Directory _settings = Directory("${Platform.environment["UserProfile"]}\\.reboot_launcher\\backend\\settings");
|
||||
|
||||
const String _engineName = "DefaultEngine.ini";
|
||||
final String _engineIni = loadEmbedded("config/$_engineName").readAsStringSync();
|
||||
|
||||
const String _gameName = "DefaultGame.ini";
|
||||
final String _gameIni = loadEmbedded("config/$_gameName").readAsStringSync();
|
||||
|
||||
const String _runtimeName = "DefaultRuntimeOptions.ini";
|
||||
final String _runtimeIni = loadEmbedded("config/$_runtimeName").readAsStringSync();
|
||||
|
||||
List<Map<String, Object>> getStorageSettings(Context context) => [
|
||||
_getStorageSetting(_engineName, _engineIni),
|
||||
_getStorageSetting(_gameName, _gameIni),
|
||||
_getStorageSetting(_runtimeName, _runtimeIni)
|
||||
];
|
||||
|
||||
Map<String, Object> _getStorageSetting(String name, String source){
|
||||
var bytes = utf8.encode(source);
|
||||
return {
|
||||
"uniqueFilename": name,
|
||||
"filename": name,
|
||||
"hash": sha1.convert(bytes).toString(),
|
||||
"hash256": sha256.convert(bytes).toString(),
|
||||
"length": bytes.length,
|
||||
"contentType": "application/octet-stream",
|
||||
"uploaded": "2020-02-23T18:35:53.967Z",
|
||||
"storageType": "S3",
|
||||
"storageIds": {},
|
||||
"doNotCache": true
|
||||
};
|
||||
}
|
||||
|
||||
Response getStorageSetting(Context context) {
|
||||
switch(context.pathParams.get("file")){
|
||||
case _engineName:
|
||||
return Response(body: _engineIni);
|
||||
case _gameName:
|
||||
return Response(body: _gameIni);
|
||||
case _runtimeName:
|
||||
return Response(body: _runtimeIni);
|
||||
default:
|
||||
return Response();
|
||||
}
|
||||
}
|
||||
|
||||
Response getStorageFile(Context context) {
|
||||
if (context.pathParams.get("file")?.toLowerCase() != "clientsettings.sav") {
|
||||
return Response.json(
|
||||
{"error": "File not found"},
|
||||
statusCode: 404
|
||||
);
|
||||
}
|
||||
|
||||
var file = _getSettingsFile(context);
|
||||
return Response(
|
||||
body: file.existsSync() ? file.readAsBytesSync() : null,
|
||||
headers: {"content-type": "application/octet-stream"}
|
||||
);
|
||||
}
|
||||
|
||||
List<Map<String, Object?>> getStorageAccount(Context context) {
|
||||
var file = _getSettingsFile(context);
|
||||
if (!file.existsSync()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
var content = file.readAsBytesSync();
|
||||
return [{
|
||||
"uniqueFilename": "ClientSettings.Sav",
|
||||
"filename": "ClientSettings.Sav",
|
||||
"hash": sha1.convert(content).toString(),
|
||||
"hash256": sha256.convert(content).toString(),
|
||||
"length": content.length,
|
||||
"contentType": "application/octet-stream",
|
||||
"uploaded": "2020-02-23T18:35:53.967Z",
|
||||
"storageType": "S3",
|
||||
"storageIds": {},
|
||||
"accountId": context.pathParams.get("accountId"),
|
||||
"doNotCache": true
|
||||
}];
|
||||
}
|
||||
|
||||
Future<Response> addStorageFile(Context context) async {
|
||||
if(!_settings.existsSync()){
|
||||
await _settings.create(recursive: true);
|
||||
}
|
||||
|
||||
var file = _getSettingsFile(context);
|
||||
await file.writeAsBytes(await context.body);
|
||||
return Response(statusCode: 204);
|
||||
}
|
||||
|
||||
File _getSettingsFile(Context context) {
|
||||
if(!_settings.existsSync()){
|
||||
_settings.createSync(recursive: true);
|
||||
}
|
||||
|
||||
return File("${_settings.path}\\ClientSettings-${parseSeasonBuild(context)}.Sav");
|
||||
}
|
||||
19
lib/src/embedded/storefront.dart
Normal file
19
lib/src/embedded/storefront.dart
Normal file
@@ -0,0 +1,19 @@
|
||||
|
||||
import 'package:jaguar/http/context/context.dart';
|
||||
import 'package:jaguar/http/response/response.dart';
|
||||
import 'package:reboot_launcher/src/util/os.dart';
|
||||
|
||||
final String _keyChain = loadEmbedded("responses/keychain.json").readAsStringSync();
|
||||
final String _catalog = loadEmbedded("responses/catalog.json").readAsStringSync();
|
||||
|
||||
Response getCatalog(Context context) {
|
||||
if (context.headers.value("user-agent")?.contains("2870186") == true) {
|
||||
return Response(statusCode: 404);
|
||||
}
|
||||
|
||||
return Response(body: _catalog, headers: {"content-type": "application/json"});
|
||||
}
|
||||
|
||||
Response getKeyChain(Context context) => Response(body: _keyChain, headers: {"content-type": "application/json"});
|
||||
|
||||
Map<String, Object> getOffers(Context context) => {};
|
||||
42
lib/src/embedded/utils.dart
Normal file
42
lib/src/embedded/utils.dart
Normal file
@@ -0,0 +1,42 @@
|
||||
import 'dart:collection';
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:jaguar/http/context/context.dart';
|
||||
|
||||
const String _chars =
|
||||
'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890';
|
||||
final Random _random = Random();
|
||||
|
||||
String randomString(int length) => String.fromCharCodes(
|
||||
Iterable.generate(length, (_) => _chars.codeUnitAt(_random.nextInt(_chars.length))));
|
||||
|
||||
double parseSeasonBuild(Context context){
|
||||
String? userAgent = context.headers.value("user-agent");
|
||||
if (userAgent == null) {
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
try {
|
||||
var build = userAgent.split("Release-")[1].split("-")[0];
|
||||
if (build.split(".").length == 3) {
|
||||
var value = build.split(".");
|
||||
return double.parse("${value[0]}.${value[1]}${value[2]}");
|
||||
}
|
||||
|
||||
return double.parse(build);
|
||||
} catch (_) {
|
||||
return 2.0;
|
||||
}
|
||||
}
|
||||
|
||||
int parseSeason(Context context) => int.parse(parseSeasonBuild(context).toString().split(".")[0]);
|
||||
|
||||
Future<HashMap<String, String?>> parseBody(Context context) async {
|
||||
var params = HashMap<String, String?>();
|
||||
utf8.decode(await context.req.body)
|
||||
.split("&")
|
||||
.map((entry) => MapEntry(entry.substring(0, entry.indexOf("=")), entry.substring(entry.indexOf("=") + 1)))
|
||||
.forEach((element) => params[element.key] = Uri.decodeQueryComponent(element.value));
|
||||
return params;
|
||||
}
|
||||
41
lib/src/embedded/version.dart
Normal file
41
lib/src/embedded/version.dart
Normal file
@@ -0,0 +1,41 @@
|
||||
import 'package:jaguar/http/context/context.dart';
|
||||
import 'package:reboot_launcher/src/util/time.dart';
|
||||
|
||||
Map<String, Object> getVersion(Context context) => {
|
||||
"app": "fortnite",
|
||||
"serverDate": "2022-11-08T18:55:52.341Z",
|
||||
"overridePropertiesVersion": "unknown",
|
||||
"cln": "17951730",
|
||||
"build": "444",
|
||||
"moduleName": "Fortnite-Core",
|
||||
"buildDate": "2021-10-27T21:00:51.697Z",
|
||||
"version": "18.30",
|
||||
"branch": "Release-18.30",
|
||||
"modules": {
|
||||
"Epic-LightSwitch-AccessControlCore": {
|
||||
"cln": "17237679",
|
||||
"build": "b2130",
|
||||
"buildDate": "2021-08-19T18:56:08.144Z",
|
||||
"version": "1.0.0",
|
||||
"branch": "trunk"
|
||||
},
|
||||
"epic-xmpp-api-v1-base": {
|
||||
"cln": "5131a23c1470acbd9c94fae695ef7d899c1a41d6",
|
||||
"build": "b3595",
|
||||
"buildDate": "2019-07-30T09:11:06.587Z",
|
||||
"version": "0.0.1",
|
||||
"branch": "master"
|
||||
},
|
||||
"epic-common-core": {
|
||||
"cln": "17909521",
|
||||
"build": "3217",
|
||||
"buildDate": "2021-10-25T18:41:12.486Z",
|
||||
"version": "3.0",
|
||||
"branch": "TRUNK"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Map<String, Object> hasUpdate(Context context) => {
|
||||
"type": "NO_UPDATE"
|
||||
};
|
||||
Reference in New Issue
Block a user