diff --git a/ArbitraryTest.cs b/ArbitraryTest.cs new file mode 100644 index 0000000..d555ecb --- /dev/null +++ b/ArbitraryTest.cs @@ -0,0 +1,8 @@ +using System; + +public class Class1 +{ + public Class1() + { + } +} diff --git a/Starlight.sln b/Starlight.sln index 73920ce..51a0c56 100644 --- a/Starlight.sln +++ b/Starlight.sln @@ -9,8 +9,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Starlight.Cli", "src\Starli EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Starlight.Launcher", "src\Starlight.Launcher\Starlight.Launcher.csproj", "{B92B746F-1372-4321-B55D-405993DF30C7}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Starlight.Rbx", "src\Starlight.Rbx\Starlight.Rbx.csproj", "{79C73613-663B-4C7E-9504-DEDFA488BAE5}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Submodules", "Submodules", "{5325689B-2D92-4055-A93D-43CC70B72B46}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HackerFramework", "submodules\HackerFramework\src\HackerFramework\HackerFramework.csproj", "{E47F58ED-1BBE-47F4-9FA9-FACD2317FE90}" @@ -35,10 +33,6 @@ Global {B92B746F-1372-4321-B55D-405993DF30C7}.Debug|x86.Build.0 = Debug|x86 {B92B746F-1372-4321-B55D-405993DF30C7}.Release|x86.ActiveCfg = Release|x86 {B92B746F-1372-4321-B55D-405993DF30C7}.Release|x86.Build.0 = Release|x86 - {79C73613-663B-4C7E-9504-DEDFA488BAE5}.Debug|x86.ActiveCfg = Debug|x86 - {79C73613-663B-4C7E-9504-DEDFA488BAE5}.Debug|x86.Build.0 = Debug|x86 - {79C73613-663B-4C7E-9504-DEDFA488BAE5}.Release|x86.ActiveCfg = Release|x86 - {79C73613-663B-4C7E-9504-DEDFA488BAE5}.Release|x86.Build.0 = Release|x86 {E47F58ED-1BBE-47F4-9FA9-FACD2317FE90}.Debug|x86.ActiveCfg = Debug|Any CPU {E47F58ED-1BBE-47F4-9FA9-FACD2317FE90}.Debug|x86.Build.0 = Debug|Any CPU {E47F58ED-1BBE-47F4-9FA9-FACD2317FE90}.Release|x86.ActiveCfg = Release|Any CPU diff --git a/src/Starlight.Cli/App.config b/src/Starlight.Cli/App.config index 57fec86..73917a8 100644 --- a/src/Starlight.Cli/App.config +++ b/src/Starlight.Cli/App.config @@ -1,18 +1,19 @@  + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Starlight.Cli/FodyWeavers.xml b/src/Starlight.Cli/FodyWeavers.xml index 09147f5..cdb3b83 100644 --- a/src/Starlight.Cli/FodyWeavers.xml +++ b/src/Starlight.Cli/FodyWeavers.xml @@ -1,4 +1,5 @@  + diff --git a/src/Starlight.Cli/Native.cs b/src/Starlight.Cli/Native.cs index 9276d2e..fe88703 100644 --- a/src/Starlight.Cli/Native.cs +++ b/src/Starlight.Cli/Native.cs @@ -1,40 +1,39 @@ using System.Runtime.InteropServices; -namespace Starlight.Cli +namespace Starlight.Cli; + +internal class Native { - internal class Native - { #if DEBUG [DllImport("kernel32.dll")] public static extern bool IsDebuggerPresent(); #endif - [DllImport("user32.dll")] - public static extern bool ShowWindow(int hWnd, int nCmdShow); + [DllImport("user32.dll")] + public static extern bool ShowWindow(int hWnd, int nCmdShow); - [DllImport("user32.dll")] - public static extern int FindWindow(string lpClassName, string lpWindowName); + [DllImport("user32.dll")] + public static extern int FindWindow(string lpClassName, string lpWindowName); - [DllImport("kernel32.dll")] - public static extern int GetConsoleWindow(); - - [DllImport("user32.dll")] - public static extern int GetWindowThreadProcessId(int hWnd, out int lpdwProcessId); + [DllImport("kernel32.dll")] + public static extern int GetConsoleWindow(); - public const uint STD_OUTPUT_HANDLE = 0xFFFFFFF5; - - [DllImport("kernel32.dll")] - public static extern uint GetStdHandle(uint nStdHandle); - - [DllImport("kernel32.dll")] - public static extern void SetStdHandle(uint nStdHandle, uint handle); - - [DllImport("kernel32.dll")] - public static extern bool AllocConsole(); + [DllImport("user32.dll")] + public static extern int GetWindowThreadProcessId(int hWnd, out int lpdwProcessId); - [DllImport("kernel32.dll")] - public static extern bool FreeConsole(); + public const uint STD_OUTPUT_HANDLE = 0xFFFFFFF5; + + [DllImport("kernel32.dll")] + public static extern uint GetStdHandle(uint nStdHandle); + + [DllImport("kernel32.dll")] + public static extern void SetStdHandle(uint nStdHandle, uint handle); + + [DllImport("kernel32.dll")] + public static extern bool AllocConsole(); + + [DllImport("kernel32.dll")] + public static extern bool FreeConsole(); - public const int SW_HIDE = 0; - } -} + public const int SW_HIDE = 0; +} \ No newline at end of file diff --git a/src/Starlight.Cli/Program.cs b/src/Starlight.Cli/Program.cs index 70aaffe..c149a2a 100644 --- a/src/Starlight.Cli/Program.cs +++ b/src/Starlight.Cli/Program.cs @@ -1,18 +1,16 @@ using System; using System.Linq; -using System.Threading; using CommandLine; using Starlight.Cli.Verbs; using Starlight.Misc; -using static Starlight.Cli.Native; -namespace Starlight.Cli +namespace Starlight.Cli; + +internal class Program { - internal class Program + static int Main(string[] args) { - static int Main(string[] args) - { - Console.Title = "Starlight CLI"; + Console.Title = "Starlight CLI"; #if DEBUG // In Debug mode: Starlight.Cli.exe [nodebug] [options], skips requirement to attach debugger. @@ -31,31 +29,30 @@ static int Main(string[] args) else args = args.Skip(1).ToArray(); #else - if (args.Length > 0 && args[0] == "debug") - { - Logger.Init(true); - args = args.Skip(1).ToArray(); - } + if (args.Length > 0 && args[0] == "debug") + { + Logger.Init(true); + args = args.Skip(1).ToArray(); + } #endif - // I do not like how this is done, but it works. - var code = Parser.Default.ParseArguments(args) - .MapResult( - (Hook x) => x.Invoke(), - (Install x) => x.Invoke(), - (Launch x) => x.Invoke(), - (RawLaunch x) => x.Invoke(), - (Unhook x) => x.Invoke(), - (Uninstall x) => x.Invoke(), - (Unlock x) => x.Invoke(), - _ => 1); + // I do not like how this is done, but it works. + var code = Parser.Default.ParseArguments(args) + .MapResult( + (Hook x) => x.Invoke(), + (Install x) => x.Invoke(), + (Verbs.Launch x) => x.Invoke(), + (RawLaunch x) => x.Invoke(), + (Unhook x) => x.Invoke(), + (Uninstall x) => x.Invoke(), + (Unlock x) => x.Invoke(), + _ => 1); #if DEBUG Console.WriteLine($"Exit code: 0x{code:X}. Press any key to exit..."); Console.ReadKey(); #endif - return code; - } + return code; } -} +} \ No newline at end of file diff --git a/src/Starlight.Cli/Verbs/Hook.cs b/src/Starlight.Cli/Verbs/Hook.cs index 3b167f0..63dd0a5 100644 --- a/src/Starlight.Cli/Verbs/Hook.cs +++ b/src/Starlight.Cli/Verbs/Hook.cs @@ -1,37 +1,36 @@ -using CommandLine; -using Starlight.Core; -using System; -using System.Reflection; +using System; using System.IO; +using System.Reflection; +using CommandLine; +using Starlight.SchemeLaunch; + +namespace Starlight.Cli.Verbs; -namespace Starlight.Cli.Verbs +[Verb("hook", HelpText = "Hook Roblox's scheme.")] +public class Hook : VerbBase { - [Verb("hook", HelpText = "Hook Roblox's scheme.")] - public class Hook : VerbBase + protected override int Init() + { + return 0; + } + + protected override int InternalInvoke() { - protected override int Init() + var binDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + if (binDir is null) { - return 0; + Console.WriteLine("Failed to get current assembly directory."); + return 1; } - protected override int InternalInvoke() + var launcherBin = Path.Combine(binDir, "Starlight.Launcher.exe"); + if (Scheme.Hook(launcherBin)) { - var binDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); - if (binDir is null) - { - Console.WriteLine("Failed to get current assembly directory."); - return 1; - } - - var launcherBin = Path.Combine(binDir, "Starlight.Launcher.exe"); - if (Scheme.Hook(launcherBin)) - { - Console.WriteLine("Hooked scheme. You can now launch with Starlight from the browser."); - return 0; - } - - Console.WriteLine("Failed to hook scheme."); - return 1; + Console.WriteLine("Hooked scheme. You can now launch with Starlight from the browser."); + return 0; } + + Console.WriteLine("Failed to hook scheme."); + return 1; } -} +} \ No newline at end of file diff --git a/src/Starlight.Cli/Verbs/Install.cs b/src/Starlight.Cli/Verbs/Install.cs index d28dea2..87439c9 100644 --- a/src/Starlight.Cli/Verbs/Install.cs +++ b/src/Starlight.Cli/Verbs/Install.cs @@ -1,43 +1,46 @@ -using CommandLine; -using System; -using Starlight.Core; +using System; +using CommandLine; +using Starlight.Bootstrap; +using Starlight.Except; -namespace Starlight.Cli.Verbs +namespace Starlight.Cli.Verbs; + +[Verb("install", HelpText = "Install a Roblox client.")] +public class Install : VerbBase { - [Verb("install", HelpText = "Install a Roblox client.")] - public class Install : VerbBase + [Option('h', "hash", Required = false, Default = null, HelpText = "The hash of the client to install.")] + public string Hash { get; set; } + + protected override int Init() { - [Option('h', "hash", Required = false, Default = null, HelpText = "The hash of the client to install.")] - public string Hash { get; set; } + if (string.IsNullOrEmpty(Hash)) + Hash = Bootstrapper.GetLatestHash(); - protected override int Init() + try { - if (string.IsNullOrEmpty(Hash)) - Hash = Bootstrapper.GetLatestHash(); - - var client = Bootstrapper.QueryClient(Hash); - if (client is null) - return 0; - + Bootstrapper.QueryClient(Hash); Console.WriteLine($"Roblox version-{Hash} is already installed."); return 1; - } + catch (ClientNotFoundException) + { + return 0; + } + } - protected override int InternalInvoke() + protected override int InternalInvoke() + { + Console.WriteLine($"Installing Roblox version-{Hash}..."); + try { - Console.WriteLine($"Installing Roblox version-{Hash}..."); - try - { - Bootstrapper.Install(Hash); - Console.WriteLine("Roblox has been installed successfully."); - return 0; - } - catch (BootstrapException ex) - { - Console.WriteLine($"Failed to install Roblox: {ex.Message}"); - return 1; - } + Bootstrapper.Install(Hash); + Console.WriteLine("Roblox has been installed successfully."); + return 0; + } + catch (BadIntegrityException ex) + { + Console.WriteLine($"Failed to install Roblox: {ex.Message}"); + return 1; } } -} +} \ No newline at end of file diff --git a/src/Starlight.Cli/Verbs/Launch.cs b/src/Starlight.Cli/Verbs/Launch.cs index 77e4c57..91b7225 100644 --- a/src/Starlight.Cli/Verbs/Launch.cs +++ b/src/Starlight.Cli/Verbs/Launch.cs @@ -1,108 +1,122 @@ -using CommandLine; -using Starlight.Core; -using Starlight.Rbx; -using Starlight.RbxApp; -using System; -using Starlight.Rbx.JoinGame; - -namespace Starlight.Cli.Verbs +using System; +using CommandLine; +using Starlight.Apis; +using Starlight.Apis.JoinGame; +using Starlight.Bootstrap; +using Starlight.Except; +using Starlight.Launch; +using Starlight.PostLaunch; + +namespace Starlight.Cli.Verbs; + +[Verb("launch", HelpText = "Launch Roblox from the command line.")] +public class Launch : VerbBase, IStarlightLaunchParams { - [Verb("launch", HelpText = "Launch Roblox from the command line.")] - public class Launch : VerbBase, IStarlightLaunchParams - { - [Option('s', "spoof", Required = false, Default = false, HelpText = "Spoof Roblox's tracking.")] - public bool Spoof { get; set; } + [Option('t', "token", Required = true, HelpText = "Roblox authentication token. Use a token here, NOT a ticket.")] + public string Token { get; set; } - [Option('h', "hash", Required = false, Default = null, HelpText = "Launch a specific hash of Roblox.")] - public string Hash { get; set; } + [Option('p', "placeid", Required = true, Default = null, HelpText = "The place ID to join.")] + public long PlaceId { get; set; } - [Option("headless", Required = false, Default = false, HelpText = "Launch Roblox in the background.")] - public bool Headless { get; set; } + [Option('j', "jobid", Required = false, Default = null, HelpText = "The server instance ID to join.")] + public Guid? JobId { get; set; } - [Option('r', "res", Required = false, Default = null, HelpText = "Set the intiial resolution of Roblox. Example: \"-r 1920x1080\"")] - public string Resolution { get; set; } + [Option('s', "spoof", Required = false, Default = false, HelpText = "Spoof Roblox's tracking.")] + public bool Spoof { get; set; } - [Option("fps-cap", Required = false, Default = 0, HelpText = "Limits the FPS of Roblox.")] - public int FpsCap { get; set; } + [Option('h', "hash", Required = false, Default = null, HelpText = "Launch a specific hash of Roblox.")] + public string Hash { get; set; } - [Option('t', "token", Required = true, HelpText = "Roblox authentication token. Use a token here, NOT a ticket.")] - public string Token { get; set; } + [Option("headless", Required = false, Default = false, HelpText = "Launch Roblox in the background.")] + public bool Headless { get; set; } - [Option('p', "placeid", Required = true, Default = null, HelpText = "The place ID to join.")] - public long PlaceId { get; set; } + [Option('r', "res", Required = false, Default = null, + HelpText = "Set the intiial resolution of Roblox. Example: \"-r 1920x1080\"")] + public string Resolution { get; set; } - [Option('j', "jobid", Required = false, Default = null, HelpText = "The server instance ID to join.")] - public Guid? JobId { get; set; } + [Option("fps-cap", Required = false, Default = 0, HelpText = "Limits the FPS of Roblox.")] + public int FpsCap { get; set; } - protected override int Init() - { - Hash = Bootstrapper.GetLatestHash(); + [Option("attach-method", Required = false, Default = AttachMethod.None, + HelpText = "The attach method for Starlight.")] + public AttachMethod AttachMethod { get; set; } - var client = Bootstrapper.QueryClient(Hash); - if (client is not null) - return 0; + protected override int Init() + { + Hash = Bootstrapper.GetLatestHash(); + try + { + Bootstrapper.QueryClient(Hash); + } + catch (ClientNotFoundException) + { Console.WriteLine($"Installing Roblox version-{Hash}..."); try { Bootstrapper.Install(Hash); } - catch (BootstrapException ex) + catch (BadIntegrityException ex) { Console.WriteLine($"Failed to install Roblox: {ex.Message}"); return 1; } - - return 0; } - protected override int InternalInvoke() + return 0; + } + + protected override int InternalInvoke() + { + var session = + Session.Login( + Token); // TODO: captcha and other bullcrap for user:pass maybe use chromium embedded framework and make user enter it + if (session is null) { - var session = Session.Login(Token); // TODO: captcha and other bullcrap for user:pass maybe use chromium embedded framework and make user enter it - if (session is null) - { - Console.WriteLine("Failed to log in: an invalid authentication token was specified."); - return 1; - } - - Console.WriteLine("Logged in as {0} ({1})", session.Username, session.UserId); - var info = new LaunchParams { Ticket = session.GetTicket() }; + Console.WriteLine("Failed to log in: an invalid authentication token was specified."); + return 1; + } - if (JobId.HasValue) - { - info.Request = new JoinRequest - { - PlaceId = PlaceId, - JobId = JobId.Value, - ReqType = JoinType.Specific - }; - } - else - { - info.Request = new JoinRequest - { - PlaceId = PlaceId, - ReqType = JoinType.Auto - }; - } - - Console.WriteLine("Launching Roblox..."); - try - { - Launcher.Launch(info, this); - } - catch (LaunchException ex) + Console.WriteLine("Logged in as {0} ({1})", session.Username, session.UserId); + var info = new LaunchParams { Ticket = session.GetTicket() }; + + if (JobId.HasValue) + info.Request = new JoinRequest { - Console.WriteLine($"Failed to launch Roblox: {ex.Message}"); - return 1; - } - catch (AppModException ex) + PlaceId = PlaceId, + JobId = JobId.Value, + ReqType = JoinType.Specific + }; + else + info.Request = new JoinRequest { - Console.WriteLine($"Launch succeeded, but failed to post-launch: {ex.Message}"); - return 1; - } + PlaceId = PlaceId, + ReqType = JoinType.Auto + }; - return 0; + Console.WriteLine("Launching Roblox..."); + try + { + Launcher.Launch(info, this); } + catch (ClientNotFoundException) + { + Console.WriteLine("Client does not exist."); + return 1; + } + catch (PostLaunchException ex) + { + Console.WriteLine($"Launch succeeded, but failed to post-launch: {ex.Message}"); + return 1; + } + catch (Exception ex) + { + Console.WriteLine(!string.IsNullOrWhiteSpace(ex.Message) + ? $"Failed to launch Roblox: {ex.GetType().Name} -> \"{ex.Message}\"" + : $"Failed to launch Roblox: {ex.GetType().Name}"); + return 1; + } + + return 0; } -} +} \ No newline at end of file diff --git a/src/Starlight.Cli/Verbs/RawLaunch.cs b/src/Starlight.Cli/Verbs/RawLaunch.cs index 28d5081..253642b 100644 --- a/src/Starlight.Cli/Verbs/RawLaunch.cs +++ b/src/Starlight.Cli/Verbs/RawLaunch.cs @@ -1,72 +1,89 @@ -using CommandLine; -using Starlight.Core; -using Starlight.RbxApp; -using System; +using System; +using CommandLine; +using Starlight.Bootstrap; +using Starlight.Except; +using Starlight.Launch; +using Starlight.PostLaunch; +using Starlight.SchemeLaunch; -namespace Starlight.Cli.Verbs +namespace Starlight.Cli.Verbs; + +[Verb("rawlaunch", HelpText = "Launch Roblox using a roblox-player scheme payload.")] +public class RawLaunch : VerbBase, IStarlightLaunchParams { - [Verb("rawlaunch", HelpText = "Launch Roblox using a roblox-player scheme payload.")] - public class RawLaunch : VerbBase, IStarlightLaunchParams - { - [Option('s', "spoof", Required = false, Default = false, HelpText = "Spoof Roblox's tracking.")] - public bool Spoof { get; set; } + [Option('p', "payload", Required = true, HelpText = "The roblox-player scheme payload.")] + public string Payload { get; set; } - [Option('h', "hash", Required = false, Default = null, HelpText = "Launch a specific hash of Roblox.")] - public string Hash { get; set; } + [Option('s', "spoof", Required = false, Default = false, HelpText = "Spoof Roblox's tracking.")] + public bool Spoof { get; set; } - [Option("headless", Required = false, Default = false, HelpText = "Launch Roblox in the background.")] - public bool Headless { get; set; } + [Option('h', "hash", Required = false, Default = null, HelpText = "Launch a specific hash of Roblox.")] + public string Hash { get; set; } - [Option('r', "res", Required = false, Default = null, HelpText = "Set the intiial resolution of Roblox. Example: \"-r 1920x1080\"")] - public string Resolution { get; set; } + [Option("headless", Required = false, Default = false, HelpText = "Launch Roblox in the background.")] + public bool Headless { get; set; } - [Option("fps-cap", Required = false, Default = 0, HelpText = "Limits the FPS of Roblox.")] - public int FpsCap { get; set; } + [Option('r', "res", Required = false, Default = null, + HelpText = "Set the intiial resolution of Roblox. Example: \"-r 1920x1080\"")] + public string Resolution { get; set; } - [Option('p', "payload", Required = true, HelpText = "The roblox-player scheme payload.")] - public string Payload { get; set; } + [Option("fps-cap", Required = false, Default = 0, HelpText = "Limits the FPS of Roblox.")] + public int FpsCap { get; set; } - protected override int Init() - { - Hash = Bootstrapper.GetLatestHash(); + [Option("attach-method", Required = false, Default = AttachMethod.None, + HelpText = "The attach method for Starlight.")] + public AttachMethod AttachMethod { get; set; } - var client = Bootstrapper.QueryClient(Hash); - if (client is not null) - return 0; - + protected override int Init() + { + Hash = Bootstrapper.GetLatestHash(); + + try + { + Bootstrapper.QueryClient(Hash); + } + catch (ClientNotFoundException) + { Console.WriteLine($"Installing Roblox version-{Hash}..."); try { Bootstrapper.Install(Hash); } - catch (BootstrapException ex) + catch (BadIntegrityException ex) { Console.WriteLine($"Failed to install Roblox: {ex.Message}"); return 1; } - - return 0; } - protected override int InternalInvoke() + return 0; + } + + protected override int InternalInvoke() + { + Console.WriteLine("Launching Roblox..."); + try { - Console.WriteLine("Launching Roblox..."); - try - { - Scheme.Launch(Payload, this); - } - catch (LaunchException ex) - { - Console.WriteLine($"Failed to launch Roblox: {ex.Message}"); - return 1; - } - catch (AppModException ex) - { - Console.WriteLine($"Launch succeeded, but failed to post-launch: {ex.Message}"); - return 1; - } - - return 0; + Scheme.Launch(Payload, this); } + catch (ClientNotFoundException) + { + Console.WriteLine("Client does not exist."); + return 1; + } + catch (PostLaunchException ex) + { + Console.WriteLine($"Launch succeeded, but failed to post-launch: {ex.Message}"); + return 1; + } + catch (Exception ex) + { + Console.WriteLine(!string.IsNullOrWhiteSpace(ex.Message) + ? $"Failed to launch Roblox: {ex.GetType().Name} -> \"{ex.Message}\"" + : $"Failed to launch Roblox: {ex.GetType().Name}"); + return 1; + } + + return 0; } -} +} \ No newline at end of file diff --git a/src/Starlight.Cli/Verbs/Unhook.cs b/src/Starlight.Cli/Verbs/Unhook.cs index fa43d00..c9ed055 100644 --- a/src/Starlight.Cli/Verbs/Unhook.cs +++ b/src/Starlight.Cli/Verbs/Unhook.cs @@ -1,32 +1,31 @@ -using CommandLine; -using Starlight.Core; -using System; +using System; +using CommandLine; +using Starlight.Bootstrap; +using Starlight.SchemeLaunch; -namespace Starlight.Cli.Verbs +namespace Starlight.Cli.Verbs; + +[Verb("unhook", HelpText = "Unhook from Roblox's scheme.")] +public class Unhook : VerbBase { - [Verb("unhook", HelpText = "Unhook from Roblox's scheme.")] - public class Unhook : VerbBase + protected override int Init() { - protected override int Init() - { - if (Bootstrapper.GetClients().Count >= 1) - return 0; - - Console.WriteLine("No Roblox clients are installed."); - return 1; + if (Bootstrapper.GetClients().Count >= 1) + return 0; - } + Console.WriteLine("No Roblox clients are installed."); + return 1; + } - protected override int InternalInvoke() + protected override int InternalInvoke() + { + if (Scheme.Unhook()) { - if (Scheme.Unhook()) - { - Console.WriteLine("Unhooked scheme. Launching Roblox from the browser should no longer open Starlight."); - return 0; - } - - Console.WriteLine("Failed to unhook scheme."); - return 1; + Console.WriteLine("Unhooked scheme. Launching Roblox from the browser should no longer open Starlight."); + return 0; } + + Console.WriteLine("Failed to unhook scheme."); + return 1; } -} +} \ No newline at end of file diff --git a/src/Starlight.Cli/Verbs/Uninstall.cs b/src/Starlight.Cli/Verbs/Uninstall.cs index d5c02c9..fcc2ecc 100644 --- a/src/Starlight.Cli/Verbs/Uninstall.cs +++ b/src/Starlight.Cli/Verbs/Uninstall.cs @@ -1,35 +1,37 @@ -using CommandLine; -using System; +using System; +using CommandLine; +using Starlight.Bootstrap; +using Starlight.Except; -using Starlight.Core; +namespace Starlight.Cli.Verbs; -namespace Starlight.Cli.Verbs +[Verb("uninstall", HelpText = "Uninstall a Roblox client.")] +public class Uninstall : VerbBase { - [Verb("uninstall", HelpText = "Uninstall a Roblox client.")] - public class Uninstall : VerbBase + [Option('h', "hash", Required = true, Default = null, HelpText = "The hash of the client to uninstall.")] + public string Hash { get; set; } + + protected override int Init() { - [Option('h', "hash", Required = true, Default = null, HelpText = "The hash of the client to uninstall.")] - public string Hash { get; set; } + if (string.IsNullOrEmpty(Hash)) + Hash = Bootstrapper.GetLatestHash(); - protected override int Init() + try + { + Bootstrapper.QueryClient(Hash); + return 0; + } + catch (ClientNotFoundException) { - if (string.IsNullOrEmpty(Hash)) - Hash = Bootstrapper.GetLatestHash(); - - var client = Bootstrapper.QueryClient(Hash); - if (client is not null) - return 0; - Console.WriteLine($"Roblox version-{Hash} doesn't exist."); return 1; - } + } - protected override int InternalInvoke() - { - Bootstrapper.Uninstall(Hash); - Console.WriteLine($"Roblox version-{Hash} has been uninstalled successfully."); - return 0; - } + protected override int InternalInvoke() + { + Bootstrapper.Uninstall(Hash); + Console.WriteLine($"Roblox version-{Hash} has been uninstalled successfully."); + return 0; } -} +} \ No newline at end of file diff --git a/src/Starlight.Cli/Verbs/Unlock.cs b/src/Starlight.Cli/Verbs/Unlock.cs index e71d8b1..7f33cb4 100644 --- a/src/Starlight.Cli/Verbs/Unlock.cs +++ b/src/Starlight.Cli/Verbs/Unlock.cs @@ -1,78 +1,77 @@ -using CommandLine; -using System; -using Starlight.Core; +using System; using System.Threading; +using CommandLine; +using Starlight.Launch; using static Starlight.Cli.Native; -namespace Starlight.Cli.Verbs +namespace Starlight.Cli.Verbs; + +[Verb("unlock", HelpText = "Enable multiple Roblox clients.")] +public class Unlock : VerbBase { - [Verb("unlock", HelpText = "Enable multiple Roblox clients.")] - public class Unlock : VerbBase - { - [Option('e', "relock", Required = false, Default = false, HelpText = "Lock when all clients close.")] - public bool Relock { get; set; } + [Option('e', "relock", Required = false, Default = false, HelpText = "Lock when all clients close.")] + public bool Relock { get; set; } - protected override int Init() - { - return 0; - } + protected override int Init() + { + return 0; + } - static void CommitThread(CancellationToken cancelToken) + static void CommitThread(CancellationToken cancelToken) + { + // I use FindWindow because scanning process list kills my CPU. + while (!cancelToken.IsCancellationRequested) { - // I use FindWindow because scanning process list kills my CPU. - while (!cancelToken.IsCancellationRequested) + while (FindWindow(null, "Roblox") == 0) // If Roblox isn't open yet, wait for open. { - while (FindWindow(null, "Roblox") == 0) // If Roblox isn't open yet, wait for open. - { - cancelToken.WaitHandle.WaitOne(TimeSpan.FromSeconds(1.0d / 15)); - if (cancelToken.IsCancellationRequested) - break; - } + cancelToken.WaitHandle.WaitOne(TimeSpan.FromSeconds(1.0d / 15)); + if (cancelToken.IsCancellationRequested) + break; + } - Launcher.CommitSingleton(); + Launcher.CommitSingleton(); - while (FindWindow(null, "Roblox") != 0) // Wait for all instances to close. - { - cancelToken.WaitHandle.WaitOne(TimeSpan.FromSeconds(1.0d / 15)); - if (cancelToken.IsCancellationRequested) - break; - } - - Launcher.ReleaseSingleton(); + while (FindWindow(null, "Roblox") != 0) // Wait for all instances to close. + { + cancelToken.WaitHandle.WaitOne(TimeSpan.FromSeconds(1.0d / 15)); + if (cancelToken.IsCancellationRequested) + break; } + + Launcher.ReleaseSingleton(); } + } - protected override int InternalInvoke() + protected override int InternalInvoke() + { + if (FindWindow(null, "Roblox") != 0) { - if (FindWindow(null, "Roblox") != 0) - { - Console.WriteLine("Waiting for all Roblox processes to close..."); - while (FindWindow(null, "Roblox") != 0) - Thread.Sleep(TimeSpan.FromSeconds(1.0d / 15)); - } - - if (Relock) - { - Launcher.CommitSingleton(); + Console.WriteLine("Waiting for all Roblox processes to close..."); + while (FindWindow(null, "Roblox") != 0) + Thread.Sleep(TimeSpan.FromSeconds(1.0d / 15)); + } - while (FindWindow(null, "Roblox") != 0) - Thread.Sleep(TimeSpan.FromSeconds(1.0d / 15)); + if (Relock) + { + Launcher.CommitSingleton(); - Launcher.ReleaseSingleton(); - } - else - { - Console.ForegroundColor = ConsoleColor.Gray; - Console.Write("Press enter to release singleton..."); + while (FindWindow(null, "Roblox") != 0) + Thread.Sleep(TimeSpan.FromSeconds(1.0d / 15)); - CancellationTokenSource commitTask = new(); - new Thread(() => CommitThread(commitTask.Token)).Start(); + Launcher.ReleaseSingleton(); + } + else + { + Console.ForegroundColor = ConsoleColor.Gray; + Console.Write("Press enter to release singleton..."); - Console.ReadLine(); - commitTask.Cancel(); // Stop the commit thread - } + CancellationTokenSource commitTask = new(); + new Thread(() => CommitThread(commitTask.Token)).Start(); - return 0; + Console.ReadLine(); + commitTask.Cancel(); // Stop the commit thread } + + return 0; } -} +} \ No newline at end of file diff --git a/src/Starlight.Cli/Verbs/VerbBase.cs b/src/Starlight.Cli/Verbs/VerbBase.cs index 9013895..0bbb92f 100644 --- a/src/Starlight.Cli/Verbs/VerbBase.cs +++ b/src/Starlight.Cli/Verbs/VerbBase.cs @@ -1,15 +1,20 @@ -namespace Starlight.Cli.Verbs +namespace Starlight.Cli.Verbs; + +public abstract class VerbBase { - public abstract class VerbBase + protected virtual int Init() { - protected virtual int Init() => 0; + return 0; + } - protected virtual int InternalInvoke() => 0; + protected virtual int InternalInvoke() + { + return 0; + } - public int Invoke() - { - var initRes = Init(); - return initRes != 0 ? initRes : InternalInvoke(); - } + public int Invoke() + { + var initRes = Init(); + return initRes != 0 ? initRes : InternalInvoke(); } -} +} \ No newline at end of file diff --git a/src/Starlight.Launcher/App.config b/src/Starlight.Launcher/App.config index 57fec86..73917a8 100644 --- a/src/Starlight.Launcher/App.config +++ b/src/Starlight.Launcher/App.config @@ -1,18 +1,19 @@  + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Starlight.Launcher/FodyWeavers.xml b/src/Starlight.Launcher/FodyWeavers.xml index daeb7c8..cdb3b83 100644 --- a/src/Starlight.Launcher/FodyWeavers.xml +++ b/src/Starlight.Launcher/FodyWeavers.xml @@ -1,8 +1,9 @@  + - - - Starlight - - + + + Starlight + + \ No newline at end of file diff --git a/src/Starlight.Launcher/LaunchParamsConfig.cs b/src/Starlight.Launcher/LaunchParamsConfig.cs new file mode 100644 index 0000000..98413b7 --- /dev/null +++ b/src/Starlight.Launcher/LaunchParamsConfig.cs @@ -0,0 +1,24 @@ +using Newtonsoft.Json; +using Starlight.Launch; +using Starlight.PostLaunch; + +namespace Starlight.Launcher; + +internal class LaunchParamsConfig : IStarlightLaunchParams +{ + [JsonProperty("saveLog")] public bool SaveLog { get; set; } + + [JsonProperty("verbose")] public bool Verbose { get; set; } + + [JsonProperty("attachMethod")] public AttachMethod AttachMethod { get; set; } + + [JsonProperty("fpsCap")] public int FpsCap { get; set; } + + [JsonProperty("resolution")] public string Resolution { get; set; } + + [JsonProperty("headless")] public bool Headless { get; set; } + + [JsonProperty("spoof")] public bool Spoof { get; set; } + + [JsonProperty("hash")] public string Hash { get; set; } +} \ No newline at end of file diff --git a/src/Starlight.Launcher/Program.cs b/src/Starlight.Launcher/Program.cs index c40a70a..1adb1e6 100644 --- a/src/Starlight.Launcher/Program.cs +++ b/src/Starlight.Launcher/Program.cs @@ -1,117 +1,122 @@ using System; using System.IO; +using System.Reflection; using System.Windows.Forms; using log4net; using Newtonsoft.Json; -using Starlight.Core; +using Starlight.Bootstrap; +using Starlight.Except; using Starlight.Misc; -using Starlight.RbxApp; +using Starlight.SchemeLaunch; -namespace Starlight.Launcher -{ - internal class LaunchParamsConfig : IStarlightLaunchParams - { - [JsonProperty("saveLog")] - public bool SaveLog { get; set; } - - [JsonProperty("verbose")] - public bool Verbose { get; set; } +namespace Starlight.Launcher; - [JsonProperty("fpsCap")] - public int FpsCap { get; set; } - - [JsonProperty("resolution")] - public string Resolution { get; set; } - - [JsonProperty("headless")] - public bool Headless { get; set; } +internal class Program +{ + // ReSharper disable once PossibleNullReferenceException + internal static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); - [JsonProperty("spoof")] - public bool Spoof { get; set; } + static LaunchParamsConfig _config; - [JsonProperty("hash")] - public string Hash { get; set; } - } - - internal class Program + static int Main(string[] args) { - // ReSharper disable once PossibleNullReferenceException - internal static readonly ILog Log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); - - static LaunchParamsConfig _config; + AppDomain.CurrentDomain.UnhandledException += (s, e) => + { + MessageBox.Show($"Fatal exception: {e.ExceptionObject}"); + }; - static int Main(string[] args) + if (args.Length < 1) { - if (args.Length < 1) - { - MessageBox.Show("You're not supposed to run this directly! Do \"Starlight.Cli.exe hook\" in the command prompt to hook Roblox's scheme.", "Starlight", MessageBoxButtons.OK, MessageBoxIcon.Error); - return 1; - } + MessageBox.Show( + "You're not supposed to run this directly! Do \"Starlight.Cli.exe hook\" in the command prompt to hook Roblox's scheme.", + "Starlight", MessageBoxButtons.OK, MessageBoxIcon.Error); + return 1; + } - var firstTime = false; - var launchCfgFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "GlobalLaunchSettings.json"); - if (!File.Exists(launchCfgFile)) // If no config file exists, create one and default it. + var firstTime = false; + var launchCfgFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "GlobalLaunchSettings.json"); + if (!File.Exists(launchCfgFile)) // If no config file exists, create one and default it. + { + firstTime = true; + _config = new LaunchParamsConfig(); + File.WriteAllText(launchCfgFile, + JsonConvert.SerializeObject(_config, new JsonSerializerSettings { Formatting = Formatting.Indented })); + } + else // Otherwise, load the config file. + { + try { - firstTime = true; - _config = new LaunchParamsConfig(); - File.WriteAllText(launchCfgFile, JsonConvert.SerializeObject(_config, new JsonSerializerSettings { Formatting = Formatting.Indented })); + _config = JsonConvert.DeserializeObject(File.ReadAllText(launchCfgFile)) ?? + new LaunchParamsConfig(); } - else // Otherwise, load the config file. + catch (JsonException) { - try - { - _config = JsonConvert.DeserializeObject(File.ReadAllText(launchCfgFile)) ?? new LaunchParamsConfig(); - } - catch (JsonException) - { - MessageBox.Show("Could not parse configuration. Ensure that your configuration contains valid JSON.", "Starlight", MessageBoxButtons.OK, MessageBoxIcon.Error); - return 1; - } + MessageBox.Show("Could not parse configuration. Ensure that your configuration contains valid JSON.", + "Starlight", MessageBoxButtons.OK, MessageBoxIcon.Error); + return 1; } + } - if (_config.SaveLog) - Logger.Init(_config.Verbose); + if (_config.SaveLog) + Logger.Init(_config.Verbose); - if (firstTime) - { - Log.Info("Welcome, new person!"); - MessageBox.Show("I believe it's your first time using Starlight. Thanks for checking out the project! When using the launcher, Roblox will update in the background (if it's not updated yet) and launch. This may take some time and I have no profiling implemented to show progress. I don't have a UI out for this project, so you'll have to bear with me until I can make one. If the installation or launch fails, a message box will pop up saying that it failed. Your launch settings can be found at Starlight's base directory called \"GlobalLaunchSettings.json\", which you can modify to set your default launch options.\n\nClick OK to launch Roblox and never show this tutorial again.", "Tutorial", MessageBoxButtons.OK, MessageBoxIcon.Information); - } + if (firstTime) + { + Log.Info("Welcome, new person!"); + MessageBox.Show( + "I believe it's your first time using Starlight. Thanks for checking out the project! When using the launcher, Roblox will update in the background (if it's not updated yet) and launch. This may take some time and I have no profiling implemented to show progress. I don't have a UI out for this project, so you'll have to bear with me until I can make one. If the installation or launch fails, a message box will pop up saying that it failed. Your launch settings can be found at Starlight's base directory called \"GlobalLaunchSettings.json\", which you can modify to set your default launch options.\n\nClick OK to launch Roblox and never show this tutorial again.", + "Tutorial", MessageBoxButtons.OK, MessageBoxIcon.Information); + } - var latest = Bootstrapper.GetLatestHash(); - if (Bootstrapper.QueryClient(latest) is null) - { - // Update Roblox - Log.Info("Updating Roblox..."); - try - { - Bootstrapper.Install(latest); - } - catch (BootstrapException ex) - { - Log.Fatal("Failed to install Roblox.", ex); - return 1; - } - } + var latest = Bootstrapper.GetLatestHash(); - Log.Info("Launching Roblox with payload..."); + try + { + Bootstrapper.QueryClient(latest); + } + catch (ClientNotFoundException) + { + Log.Info("Updating Roblox..."); try { - var inst = Scheme.Launch(args[0], _config); - return inst is null ? 1 : 0; + Bootstrapper.Install(latest); } - catch (LaunchException ex) + catch (BadIntegrityException ex) { - Log.Fatal("Failed to launch Roblox.", ex); - } - catch (AppModException ex) - { - // I wouldn't say fatal here because Roblox did open. - Log.Error("Launch succeeded, but failed to post-launch.", ex); + Log.Fatal("Failed to install Roblox.", ex); + return 1; } + } - MessageBox.Show($"Failed to launch Roblox.\nLog can be viewed at {Logger.LogFile}.", "Starlight", MessageBoxButtons.OK, MessageBoxIcon.Error); - return 1; + Log.Info("Launching Roblox with payload..."); + try + { + var inst = Scheme.Launch(args[0], _config); + return inst is null ? 1 : 0; + } + catch (SchemeParseException) + { + Log.Error("Failed to parse scheme."); } + catch (ClientNotFoundException) + { + Log.Error("Roblox client not found."); + } + catch (PostLaunchException ex) + { + // I wouldn't say fatal here because Roblox did open. + Log.Error("Launch succeeded, but failed to post-launch.", ex); + } + catch (Exception ex) + { + Log.Error(!string.IsNullOrWhiteSpace(ex.Message) + ? $"Failed to launch Roblox: {ex.GetType().Name} -> \"{ex.Message}\"" + : $"Failed to launch Roblox: {ex.GetType().Name}", ex); + } + + MessageBox.Show($"Failed to launch Roblox.\nLog can be viewed at {Logger.LogFile}.", "Starlight", + MessageBoxButtons.OK, MessageBoxIcon.Error); + + return 1; } -} +} \ No newline at end of file diff --git a/src/Starlight.Rbx/Session.cs b/src/Starlight.Rbx/Session.cs deleted file mode 100644 index dfd49a6..0000000 --- a/src/Starlight.Rbx/Session.cs +++ /dev/null @@ -1,136 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using RestSharp; -using Newtonsoft.Json.Linq; -using System.Net; - -namespace Starlight.Rbx -{ - public class Session - { - /* Clients */ - - internal readonly RestClient GeneralClient = new("https://www.roblox.com/"); - - internal readonly RestClient AuthClient = new("https://auth.roblox.com/"); - - internal readonly RestClient GameClient = new("https://gamejoin.roblox.com/"); - - /* Tokens */ - - string _authToken; - protected string AuthToken - { - get => _authToken; - set - { - GeneralClient.AddCookie(".ROBLOSECURITY", value, "/", ".roblox.com"); - - AuthClient.AddCookie(".ROBLOSECURITY", value, "/", ".roblox.com"); - - GameClient.AddCookie(".ROBLOSECURITY", value, "/", ".roblox.com"); - - _authToken = value; - } - } - - public async Task ValidateAsync() - { - var req = new RestRequest("/v2/logout", Method.Post); - var res = await AuthClient.ExecuteAsync(req); - - return res.StatusCode != HttpStatusCode.Unauthorized; // Forbidden means no xsrf token, unauthorized means no authentication token - } - - public bool Validate() => - ValidateAsync().Result; - - string _xsrfToken; - DateTime _xsrfLastGrabbed; - - public string GetXsrfToken(bool ignoreAliveCheck = false) - { - if (!ignoreAliveCheck && string.IsNullOrEmpty(_xsrfToken) && (DateTime.Now - _xsrfLastGrabbed).TotalMinutes < 2) // Return previous token if session is still alive - return _xsrfToken; - - var req = new RestRequest("/v2/logout", Method.Post); // won't log you out without a xsrf token - var res = AuthClient.Execute(req); - - var xsrfHeader = res.Headers?.FirstOrDefault(x => x.Name == "x-csrf-token"); - if (xsrfHeader is null) - throw new Exception("Failed to get xsrf token"); - - _xsrfToken = xsrfHeader.Value?.ToString(); - _xsrfLastGrabbed = DateTime.Now; - - return _xsrfToken; - } - - public async Task GetXsrfTokenAsync(bool ignoreAliveCheck = false) => - await Task.Run(() => GetXsrfToken(ignoreAliveCheck)); - - /* Session Info */ - - public string UserId { get; protected set; } - - public string Username { get; protected set; } - - void RetrieveInfo() - { - var req = new RestRequest("/my/settings/json"); - var res = GeneralClient.Execute(req); - - var body = JObject.Parse(res.Content); - UserId = body["UserId"].ToString(); - Username = body["Name"].ToString(); - } - - /* Session Management */ - - public static Session Login(string authToken) - { - var session = new Session { AuthToken = authToken }; - - if (!session.Validate()) - return null; - session.RetrieveInfo(); - - return session; - } - - public static async Task LoginAsync(string authToken) => - await Task.Run(() => Login(authToken)); - - /* Tickets */ - - public string GetTicket() - { - var req = new RestRequest("/v1/authentication-ticket", Method.Post) - .AddHeader("X-CSRF-TOKEN", GetXsrfToken()) - .AddHeader("Referer", "https://www.roblox.com/"); // hehehe - - var res = AuthClient.Execute(req); - - var ticketHeader = res.Headers?.FirstOrDefault(x => x.Name == "rbx-authentication-ticket"); - if (ticketHeader is null) - throw new Exception("Failed to get auth ticket"); - - return ticketHeader.Value?.ToString(); - } - - public async Task GetTicketAsync() => - await Task.Run(GetTicket); - - Session() { } - - ~Session() - { - GeneralClient.Dispose(); - - AuthClient.Dispose(); - - GameClient.Dispose(); - } - } -} diff --git a/src/Starlight.Test/BootstrapperTests.cs b/src/Starlight.Test/BootstrapperTests.cs index a697d05..2762d76 100644 --- a/src/Starlight.Test/BootstrapperTests.cs +++ b/src/Starlight.Test/BootstrapperTests.cs @@ -1,106 +1,106 @@ -using NUnit.Framework; -using Starlight.Core; -using Starlight.Misc; -using System; +using System; using System.IO; using System.Threading; +using NUnit.Framework; +using Starlight.Bootstrap; +using Starlight.Except; +using Starlight.Misc; + +namespace Starlight.Test; -namespace Starlight.Test +[TestFixture] +public class BootstrapperTests { - [TestFixture] - public class BootstrapperTests + [OneTimeSetUp] + public void InitLogger() { - [OneTimeSetUp] - public void InitLogger() - { - Logger.Init(true); - } + Logger.Init(true); + } - [Test] - public void GetLatestHash() + [Test] + public void GetLatestHash() + { + try { - try - { - Bootstrapper.GetLatestHash(); - } - catch (BootstrapException) - { - Assert.Inconclusive("Couldn't fetch latest hash."); - } + Bootstrapper.GetLatestHash(); } - - [Test] - public void FetchManifest() + catch (BadIntegrityException) { - var manifest = Bootstrapper.GetManifest(Bootstrapper.GetLatestHash()); - if (manifest is null) - Assert.Inconclusive("Couldn't fetch manifest."); + Assert.Inconclusive("Couldn't fetch latest hash."); } + } - static void PreNativeInstall() - { - var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); - var installPath = Path.Combine(localAppData, "Roblox"); + [Test] + public void FetchManifest() + { + var manifest = Bootstrapper.GetManifest(Bootstrapper.GetLatestHash()); + if (manifest is null) + Assert.Inconclusive("Couldn't fetch manifest."); + } - // Uninstall Roblox if it's installed. - if (Directory.Exists(installPath)) - Directory.Delete(installPath, true); - } + static void PreNativeInstall() + { + var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + var installPath = Path.Combine(localAppData, "Roblox"); + + // Uninstall Roblox if it's installed. + if (Directory.Exists(installPath)) + Directory.Delete(installPath, true); + } + + [Test] + public void NativeInstall() + { + Env.AssertCi(); - [Test] - public void NativeInstall() + PreNativeInstall(); + try { - Env.AssertCi(); - - PreNativeInstall(); - try - { - var client = Bootstrapper.NativeInstall(); - Assert.IsFalse(client is null, "Client installation failed."); - } - catch (BootstrapException ex) - { - Assert.Fail("Client installation failed: " + ex.Message, ex); - } - Thread.Sleep(1000); // idk bruh it works lol + var client = Bootstrapper.NativeInstall(); + Assert.IsFalse(client is null, "Client installation failed."); } - - [Test] - public void Query() + catch (BadIntegrityException ex) { - Env.AssertCi(); + Assert.Fail("Client installation failed due to integrity check fail.", ex); + } - if (Bootstrapper.GetClients().Count < 1) - Bootstrapper.Install(); + Thread.Sleep(1000); // idk bruh it works lol + } - var clients = Bootstrapper.GetClients(); - if (clients.Count < 1) - Assert.Fail("Failed to find any clients."); - - var client = Bootstrapper.QueryClient(clients[0].Hash); - Assert.IsFalse(client is null, "Client query failed."); - } + [Test] + public void Query() + { + Env.AssertCi(); - [Test] - public void Uninstall() - { - Env.AssertCi(); + if (Bootstrapper.GetClients().Count < 1) + Bootstrapper.Install(); - if (Bootstrapper.GetClients().Count < 1) - Bootstrapper.Install(); + var clients = Bootstrapper.GetClients(); + if (clients.Count < 1) + Assert.Fail("Failed to find any clients."); - var clients = Bootstrapper.GetClients(); - Bootstrapper.Uninstall(clients[0]); - } + Bootstrapper.QueryClient(clients[0].Hash); + } - [Test] - public void Install() - { - Env.AssertCi(); + [Test] + public void Uninstall() + { + Env.AssertCi(); - var client = Bootstrapper.Install(); - if (client is null) - Assert.Fail("Client installation failed."); - } + if (Bootstrapper.GetClients().Count < 1) + Bootstrapper.Install(); + + var clients = Bootstrapper.GetClients(); + Bootstrapper.Uninstall(clients[0]); + } + + [Test] + public void Install() + { + Env.AssertCi(); + + var client = Bootstrapper.Install(); + if (client is null) + Assert.Fail("Client installation failed."); } -} +} \ No newline at end of file diff --git a/src/Starlight.Test/Env.cs b/src/Starlight.Test/Env.cs index e0d583a..d44a93d 100644 --- a/src/Starlight.Test/Env.cs +++ b/src/Starlight.Test/Env.cs @@ -1,18 +1,17 @@ using System; using NUnit.Framework; -namespace Starlight.Test +namespace Starlight.Test; + +internal class Env { - internal class Env - { - public static bool IsCi => Environment.GetEnvironmentVariable("IS_CI") is not null; + public static bool IsCi => Environment.GetEnvironmentVariable("IS_CI") is not null; - public static string AuthToken => Environment.GetEnvironmentVariable("AUTH_TOKEN"); + public static string AuthToken => Environment.GetEnvironmentVariable("AUTH_TOKEN"); - public static void AssertCi() - { - if (IsCi) - Assert.Inconclusive("Cannot run this test on CI."); - } + public static void AssertCi() + { + if (IsCi) + Assert.Inconclusive("Cannot run this test on CI."); } -} +} \ No newline at end of file diff --git a/src/Starlight.Test/Initial.cs b/src/Starlight.Test/Initial.cs index e7e7605..3eeed4e 100644 --- a/src/Starlight.Test/Initial.cs +++ b/src/Starlight.Test/Initial.cs @@ -2,17 +2,17 @@ using NUnit.Framework; using Starlight.Misc; -namespace Starlight.Test +namespace Starlight.Test; + +[TestFixture] +[Order(1)] +internal class Initial { - [TestFixture, Order(1)] - internal class Initial + [OneTimeSetUp] + public void Initialize() { - [OneTimeSetUp] - public void Initialize() - { - Logger.Init(true); - if (Env.IsCi) - Console.WriteLine("Running on CI. ENSURE THAT INCONCLUSIVE TESTS PASS BEFORE YOU MERGE A PR!"); - } + Logger.Init(true); + if (Env.IsCi) + Console.WriteLine("Running on CI. ENSURE THAT INCONCLUSIVE TESTS PASS BEFORE YOU MERGE A PR!"); } -} +} \ No newline at end of file diff --git a/src/Starlight.Test/LauncherTests.cs b/src/Starlight.Test/LauncherTests.cs index f03cc84..1f9935d 100644 --- a/src/Starlight.Test/LauncherTests.cs +++ b/src/Starlight.Test/LauncherTests.cs @@ -1,50 +1,51 @@ using NUnit.Framework; -using Starlight.Core; -using Starlight.Rbx; -using Starlight.Rbx.JoinGame; +using Starlight.Apis; +using Starlight.Apis.JoinGame; +using Starlight.Bootstrap; +using Starlight.Launch; -namespace Starlight.Test +namespace Starlight.Test; + +[TestFixture] +[Order(2)] +public class LauncherTests { - [TestFixture, Order(2)] - public class LauncherTests + [Test] + public void Launch() { - [Test] - public void Launch() - { - Env.AssertCi(); + Env.AssertCi(); - if (Bootstrapper.GetClients().Count < 1) - Bootstrapper.Install(); + if (Bootstrapper.GetClients().Count < 1) + Bootstrapper.Install(); - if (Env.AuthToken is null) - Assert.Inconclusive("No Roblox token was provided. Please set the AUTH_TOKEN environment variable."); + if (Env.AuthToken is null) + Assert.Inconclusive("No Roblox token was provided. Please set the AUTH_TOKEN environment variable."); - var session = Session.Login(Env.AuthToken); - var ticket = session.GetTicket(); + var session = Session.Login(Env.AuthToken); + var ticket = session.GetTicket(); - var info = new LaunchParams + var info = new LaunchParams + { + // Roblox join stuff. + Ticket = ticket, + Request = new JoinRequest { - // Roblox join stuff. - Ticket = ticket, - Request = new JoinRequest - { - PlaceId = 4483381587, // https://www.roblox.com/games/4483381587/a-literal-baseplate - ReqType = JoinType.Auto - }, - - // Should make everything run - FpsCap = 160, - Headless = true, - Spoof = true, - Resolution = "800x600" - }; - - // Launch Roblox - var inst = Launcher.Launch(info); - if (inst is null) - Assert.Fail("Failed to launch Roblox."); - - inst.Proc.Kill(); - } + PlaceId = 4483381587, // https://www.roblox.com/games/4483381587/a-literal-baseplate + ReqType = JoinType.Auto + }, + + // Should make everything run + FpsCap = 160, + Headless = true, + Spoof = true, + Resolution = "800x600" + }; + + // Launch Roblox + var inst = Launcher.Launch(info); + if (inst is null) + Assert.Fail("Failed to launch Roblox."); + + inst.Proc.Kill(); } -} +} \ No newline at end of file diff --git a/src/Starlight.Test/Starlight.Test.csproj b/src/Starlight.Test/Starlight.Test.csproj index 1470117..4f860a9 100644 --- a/src/Starlight.Test/Starlight.Test.csproj +++ b/src/Starlight.Test/Starlight.Test.csproj @@ -26,7 +26,6 @@ - \ No newline at end of file diff --git a/src/Starlight/Apis/JoinGame/JoinRequest.cs b/src/Starlight/Apis/JoinGame/JoinRequest.cs new file mode 100644 index 0000000..7678179 --- /dev/null +++ b/src/Starlight/Apis/JoinGame/JoinRequest.cs @@ -0,0 +1,225 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using System.Web; +using Newtonsoft.Json; +using RestSharp; + +namespace Starlight.Apis.JoinGame; + +public class JoinRequest +{ + [JsonIgnore] internal Dictionary Options; + // This file is very cancerous. Proceed with caution. + + [JsonIgnore] public JoinType? ReqType = JoinType.Auto; + + public JoinRequest() + { + Options = new Dictionary(); + } + + public JoinRequest(Uri launchUri) + { + var query = HttpUtility.ParseQueryString(launchUri.Query); + Options = query.Cast().ToDictionary(k => k, k => query[k]); + } + + [JsonIgnore] + internal string Endpoint + { + get + { + return ReqType switch // gamejoin.roblox.com + { + JoinType.Auto => "v1/join-game", + JoinType.Specific => "/v1/join-game-instance", + JoinType.Private => "/v1/join-private-game", + _ => null + }; + } + } + + [JsonIgnore] + internal string RequestName + { + get + { + return ReqType switch + { + JoinType.Auto => "RequestGame", + JoinType.Specific => "RequestGame", + JoinType.Private => "RequestPrivateGame", + _ => null + }; + } + } + + [JsonProperty("gameId")] + public Guid? JobId + { + get + { + if (Options.TryGetValue("gameId", out var gameId) && Guid.TryParse(gameId, out var x)) + return x; + return null; + } + set + { + if (value is null) + Options.Remove("gameId"); + else + Options["gameId"] = value.ToString(); + } + } + + [JsonProperty("isTeleport")] + public bool? IsTeleport + { + get + { + if (Options.TryGetValue("isTeleport", out var isTeleport) && bool.TryParse(isTeleport, out var x)) + return x; + return null; + } + set + { + if (value is null) + Options.Remove("isTeleport"); + else + Options["isTeleport"] = value.ToString(); + } + } + + [JsonProperty("placeId")] + public long? PlaceId + { + get + { + if (Options.TryGetValue("placeId", out var placeId) && long.TryParse(placeId, out var x)) + return x; + return null; + } + set + { + if (value is null) + Options.Remove("placeId"); + else + Options["placeId"] = value.ToString(); + } + } + + [JsonProperty("accessCode")] + public Guid? AccessCode + { + get + { + if (Options.TryGetValue("accessCode", out var accessCode) && Guid.TryParse(accessCode, out var x)) + return x; + return null; + } + set + { + if (value is null) + Options.Remove("accessCode"); + else + Options["accessCode"] = value.ToString(); + } + } + + [JsonProperty("linkCode")] + public string LinkCode + { + get + { + Options.TryGetValue("linkCode", out var x); + return x; + } + set + { + if (value is null) + Options.Remove("linkCode"); + else + Options["linkCode"] = value; + } + } + + [JsonProperty("isPlayTogetherGame")] + public bool? IsPlayTogetherGame + { + get + { + if (Options.TryGetValue("isPlayTogetherGame", out var playTogether) && + bool.TryParse(playTogether, out var x)) + return x; + return null; + } + set + { + if (value is null) + Options.Remove("isPlayTogetherGame"); + else + Options["isPlayTogetherGame"] = value.ToString(); + } + } + + [JsonProperty("browserTrackerId")] + public long? BrowserTrackerId + { + get + { + if (Options.TryGetValue("browserTrackerId", out var tracker) && long.TryParse(tracker, out var x)) + return x; + return null; + } + set + { + if (value is null) + Options.Remove("browserTrackerId"); + else + Options["browserTrackerId"] = value.ToString(); + } + } + + public async Task ExecuteAsync(Session session, int maxTries = 0) + { + var tries = -1; + for (;;) + { + var reqBody = JsonConvert.SerializeObject(this, Formatting.None, + new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }); + var req = new RestRequest(Endpoint, Method.Post).AddHeader("Content-Type", "application/json") + .AddHeader("User-Agent", "Roblox/WinInet") // pov you use undocumented endpoints + .AddBody(reqBody); + + var res = await session.GameClient + .ExecuteAsync(req); // response body should look like this: https://i.imgur.com/GZChdWo.png + var body = JsonConvert.DeserializeObject(res.Content); + + if (body.Status == JoinStatus.Retry && tries != maxTries) + { + await Task.Delay(2000); // Wait a bit. + tries++; + continue; + } + + if (res.StatusCode == HttpStatusCode.OK && body.Status != JoinStatus.Fail) body.Success = true; + + return body; + } + } + + public JoinResponse Execute(Session session, int maxTries = 0) + { + return ExecuteAsync(session, maxTries).Result; + } + + public string Serialize() + { + const string urlTemplate = "https://assetgame.roblox.com/game/PlaceLauncher.ashx?{0}"; + return string.Format(urlTemplate, + string.Join("&", Options.Select(x => $"{x.Key}={HttpUtility.UrlEncode(x.Value.ToString())}"))); + } +} \ No newline at end of file diff --git a/src/Starlight/Apis/JoinGame/JoinResponse.cs b/src/Starlight/Apis/JoinGame/JoinResponse.cs new file mode 100644 index 0000000..407e7f5 --- /dev/null +++ b/src/Starlight/Apis/JoinGame/JoinResponse.cs @@ -0,0 +1,33 @@ +using Newtonsoft.Json; + +namespace Starlight.Apis.JoinGame; + +public class JoinResponse +{ + [JsonProperty("status")] internal int? InternalStatus; + + [JsonProperty("joinScript")] public JoinScript JoinScript; + + [JsonProperty("joinScriptUrl")] public string JoinScriptUrl; + + [JsonProperty("message")] public string Message; + + [JsonIgnore] public bool Success; + + [JsonIgnore] + public JoinStatus Status + { + get + { + return InternalStatus switch + { + 0 => JoinStatus.Retry, + 1 => JoinStatus.Retry, + 2 => JoinStatus.Success, + 6 => JoinStatus.FullGame, + 10 => JoinStatus.UserLeft, + _ => JoinStatus.Fail + }; + } + } +} \ No newline at end of file diff --git a/src/Starlight/Apis/JoinGame/JoinScript.cs b/src/Starlight/Apis/JoinGame/JoinScript.cs new file mode 100644 index 0000000..f0ff965 --- /dev/null +++ b/src/Starlight/Apis/JoinGame/JoinScript.cs @@ -0,0 +1,40 @@ +using System.Linq; +using System.Net; +using Newtonsoft.Json; + +namespace Starlight.Apis.JoinGame; + +public class JoinScript +{ + // IGNORE INTELLISENSE IT'S LEGALLY BLIND + + [JsonProperty("MachineAddress")] internal string Address; + + [JsonProperty("ServerPort")] internal int? Port; + + [JsonProperty("UdmuxEndpoints")] internal UdmuxEndpoint[] UdmuxEndpoints; + + public IPEndPoint GetEndpoint() + { + IPAddress ipAddr; + int port; + + if + (UdmuxEndpoints is not null) // No clue what this is but I tested it and it was requesting to the endpoint there. MachineAddress here would be a private IP so all I can think is that it's proxied or just weird like Roblox web devs made it. + { + var endpoint = UdmuxEndpoints.FirstOrDefault(); + if (endpoint is null) + return null; + + IPAddress.TryParse(endpoint.Address, out ipAddr); + port = endpoint.Port.GetValueOrDefault(); + } + else + { + IPAddress.TryParse(Address, out ipAddr); + port = Port.GetValueOrDefault(); + } + + return new IPEndPoint(ipAddr, port); + } +} \ No newline at end of file diff --git a/src/Starlight/Apis/JoinGame/JoinStatus.cs b/src/Starlight/Apis/JoinGame/JoinStatus.cs new file mode 100644 index 0000000..134e680 --- /dev/null +++ b/src/Starlight/Apis/JoinGame/JoinStatus.cs @@ -0,0 +1,10 @@ +namespace Starlight.Apis.JoinGame; + +public enum JoinStatus +{ + Fail, // Request failed/rejected. + Retry, // Request acknowledged, either standby for a server to be available or just retry. + Success, // Request accepted. + FullGame, // Request acknowledged, but the game is full. + UserLeft // When joining another user: the user left the game before you could join. I dislike this. +} \ No newline at end of file diff --git a/src/Starlight/Apis/JoinGame/JoinType.cs b/src/Starlight/Apis/JoinGame/JoinType.cs new file mode 100644 index 0000000..7956f1e --- /dev/null +++ b/src/Starlight/Apis/JoinGame/JoinType.cs @@ -0,0 +1,8 @@ +namespace Starlight.Apis.JoinGame; + +public enum JoinType +{ + Auto, + Specific, + Private +} \ No newline at end of file diff --git a/src/Starlight/Apis/JoinGame/UdmuxEndpoint.cs b/src/Starlight/Apis/JoinGame/UdmuxEndpoint.cs new file mode 100644 index 0000000..d6cd5f9 --- /dev/null +++ b/src/Starlight/Apis/JoinGame/UdmuxEndpoint.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace Starlight.Apis.JoinGame; + +public class UdmuxEndpoint +{ + [JsonProperty("Address")] public string Address; + + [JsonProperty("Port")] public int? Port; +} \ No newline at end of file diff --git a/src/Starlight/Apis/Pages/Page.cs b/src/Starlight/Apis/Pages/Page.cs new file mode 100644 index 0000000..e8059a2 --- /dev/null +++ b/src/Starlight/Apis/Pages/Page.cs @@ -0,0 +1,119 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; +using RestSharp; + +namespace Starlight.Apis.Pages; + +public class Page +{ + readonly RestClient _client; + readonly PageOptions _options; + + readonly string _path; + + string _nextPageCursor; + + int _offset; + + string _previousPageCursor; + + JToken _token; + + public Page(RestClient client, string path, PageOptions opt = null) + { + _client = client; + _path = path; + _options = opt ?? new PageOptions(); + } + + async Task InternalFetch() + { + if (_token is not null) return; + + var req = new RestRequest(_path) + .AddQueryParameter("limit", _options.Limit) + .AddQueryParameter("cursor", _options.Cursor); + + foreach (var kv in _options.Other) + req.AddQueryParameter(kv.Key, kv.Value); + + var res = await _client.ExecuteAsync(req); + if (res.Content != null) + { + var parsed = JObject.Parse(res.Content); + + _previousPageCursor = parsed["previousPageCursor"]?.ToString(); + _nextPageCursor = parsed["nextPageCursor"]?.ToString(); + + _token = parsed["data"]; + } + } + + public async Task> FetchAsync() + { + await InternalFetch(); + return _token.Select(item => item.ToObject()).ToList(); + } + + public List Fetch() + { + return FetchAsync().Result; + } + + public async Task> GetNextAsync(int max = 0) + { + await InternalFetch(); + + int i; + var data = new List(); + for (i = _offset; i < _token.Count(); i++) + { + if (max != 0 && i >= max) + break; + data.Add(_token[i]!.ToObject()); + } + + _offset = i; + + if (string.IsNullOrEmpty(_nextPageCursor)) + return data; + + if (max != 0 && i != max) + data.AddRange(await (await NextAsync()).GetNextAsync(max - i)); + else if (max == 0) + data.AddRange(await (await NextAsync()).GetNextAsync()); + + return data; + } + + public List GetNext(int max = 0) + { + return GetNextAsync(max).Result; + } + + public async Task> NextAsync() + { + await InternalFetch(); + _options.Cursor = _nextPageCursor; + return new Page(_client, _path, _options); + } + + public Page Next() + { + return NextAsync().Result; + } + + public async Task> PreviousAsync() + { + await InternalFetch(); + _options.Cursor = _previousPageCursor; + return new Page(_client, _path, _options); + } + + public Page Previous() + { + return PreviousAsync().Result; + } +} \ No newline at end of file diff --git a/src/Starlight/Apis/Pages/PageOptions.cs b/src/Starlight/Apis/Pages/PageOptions.cs new file mode 100644 index 0000000..7773ea7 --- /dev/null +++ b/src/Starlight/Apis/Pages/PageOptions.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; + +namespace Starlight.Apis.Pages; + +public class PageOptions +{ + internal readonly Dictionary Other = new(); + + public PageOptions() + { + } + + public PageOptions(int limit, string cursor) + { + Limit = limit; + Cursor = cursor; + } + + public PageOptions(string cursor, int limit) + { + Limit = limit; + Cursor = cursor; + } + + public int Limit { get; set; } = 100; + + public string Cursor { get; set; } + + public void Add(string key, object value) + { + Other[key] = value.ToString(); + } +} \ No newline at end of file diff --git a/src/Starlight/Apis/Session.cs b/src/Starlight/Apis/Session.cs new file mode 100644 index 0000000..176983a --- /dev/null +++ b/src/Starlight/Apis/Session.cs @@ -0,0 +1,151 @@ +using System; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; +using RestSharp; + +namespace Starlight.Apis; + +public class Session +{ + internal readonly RestClient AuthClient = new("https://auth.roblox.com/"); + + internal readonly RestClient GameClient = new("https://gamejoin.roblox.com/"); + /* Clients */ + + internal readonly RestClient GeneralClient = new("https://www.roblox.com/"); + + /* Tokens */ + + string _authToken; + DateTime _xsrfLastGrabbed; + + string _xsrfToken; + + Session() + { + } + + protected string AuthToken + { + get => _authToken; + set + { + GeneralClient.AddCookie(".ROBLOSECURITY", value, "/", ".roblox.com"); + + AuthClient.AddCookie(".ROBLOSECURITY", value, "/", ".roblox.com"); + + GameClient.AddCookie(".ROBLOSECURITY", value, "/", ".roblox.com"); + + _authToken = value; + } + } + + /* Session Info */ + + public string UserId { get; protected set; } + + public string Username { get; protected set; } + + public async Task ValidateAsync() + { + var req = new RestRequest("/v2/logout", Method.Post); + var res = await AuthClient.ExecuteAsync(req); + + return + res.StatusCode != + HttpStatusCode.Unauthorized; // Forbidden means no xsrf token, unauthorized means no authentication token + } + + public bool Validate() + { + return ValidateAsync().Result; + } + + public string GetXsrfToken(bool ignoreAliveCheck = false) + { + if (!ignoreAliveCheck && string.IsNullOrEmpty(_xsrfToken) && + (DateTime.Now - _xsrfLastGrabbed).TotalMinutes < 2) // Return previous token if session is still alive + return _xsrfToken; + + var req = new RestRequest("/v2/logout", Method.Post); // won't log you out without a xsrf token + var res = AuthClient.Execute(req); + + var xsrfHeader = res.Headers?.FirstOrDefault(x => x.Name == "x-csrf-token"); + if (xsrfHeader is null) + throw new Exception("Failed to get xsrf token"); + + _xsrfToken = xsrfHeader.Value?.ToString(); + _xsrfLastGrabbed = DateTime.Now; + + return _xsrfToken; + } + + public async Task GetXsrfTokenAsync(bool ignoreAliveCheck = false) + { + return await Task.Run(() => GetXsrfToken(ignoreAliveCheck)); + } + + void RetrieveInfo() + { + var req = new RestRequest("/my/settings/json"); + var res = GeneralClient.Execute(req); + + if (res.Content == null) + return; + + var body = JObject.Parse(res.Content); + UserId = body["UserId"]?.ToString(); + Username = body["Name"]?.ToString(); + } + + /* Session Management */ + + public static Session Login(string authToken) + { + var session = new Session { AuthToken = authToken }; + + if (!session.Validate()) + return null; + session.RetrieveInfo(); + + return session; + } + + public static async Task LoginAsync(string authToken) + { + return await Task.Run(() => Login(authToken)); + } + + /* Tickets */ + + public string GetTicket() + { + var req = new RestRequest("/v1/authentication-ticket", Method.Post) + .AddHeader("X-CSRF-TOKEN", GetXsrfToken()) + .AddHeader("Referer", "https://www.roblox.com/"); // hehehe + + var res = AuthClient.Execute(req); + + var ticketHeader = res.Headers?.FirstOrDefault(x => x.Name == "rbx-authentication-ticket"); + if (ticketHeader is null) + throw new Exception("Failed to get auth ticket"); + + return ticketHeader.Value?.ToString(); + } + + public async Task GetTicketAsync() + { + return await Task.Run(GetTicket); + } + + ~Session() + { + GeneralClient.Dispose(); + + AuthClient.Dispose(); + + GameClient.Dispose(); + } +} \ No newline at end of file diff --git a/src/Starlight/Bootstrap/Bootstrapper.cs b/src/Starlight/Bootstrap/Bootstrapper.cs new file mode 100644 index 0000000..2e330aa --- /dev/null +++ b/src/Starlight/Bootstrap/Bootstrapper.cs @@ -0,0 +1,494 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using System.Web; +using log4net; +using Starlight.Except; +using Starlight.Misc; +using static Starlight.Misc.Shared; +using static Starlight.Misc.Native; + +namespace Starlight.Bootstrap; + +public class Bootstrapper +{ + // ReSharper disable once PossibleNullReferenceException + internal static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + + static readonly IReadOnlyDictionary ZipMap = new Dictionary + { + { "content-avatar.zip", "content\\avatar" }, + { "content-configs.zip", "content\\configs" }, + { "content-fonts.zip", "content\\fonts" }, + { "content-models.zip", "content\\models" }, + { "content-platform-fonts.zip", "PlatformContent\\pc\\fonts" }, + { "content-sky.zip", "content\\sky" }, + { "content-sounds.zip", "content\\sounds" }, + { "content-terrain.zip", "PlatformContent\\pc\\terrain" }, + { "content-textures2.zip", "content\\textures" }, + { "content-textures3.zip", "PlatformContent\\pc\\textures" }, + { "extracontent-luapackages.zip", "ExtraContent\\LuaPackages" }, + { "extracontent-models.zip", "ExtraContent\\models" }, + { "extracontent-places.zip", "ExtraContent\\places" }, + { "extracontent-textures.zip", "ExtraContent\\textures" }, + { "extracontent-translations.zip", "ExtraContent\\translations" }, + { "RobloxApp.zip", "." }, + { "shaders.zip", "shaders" }, + { "ssl.zip", "ssl" } + }; + + static string _latestHash; // Cache for a micro-optimization + + static readonly string RobloxPath = + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Roblox"); + + static readonly string InstallationPath = Path.Combine(RobloxPath, "Versions"); + + static readonly List SkippedClients = new(); + + /// + /// Fetch an installed Roblox client with its hash. + /// + /// The hash of the Roblox client. + /// The corresponding client to the given hash. + /// Thrown when there is no client with the specified hash. + public static Client QueryClient(string hash) + { + var client = GetClients().FirstOrDefault(x => x.Hash == hash); + if (client is null) + throw new ClientNotFoundException(hash); + return client; + } + + /// + /// Fetch the latest hash from Roblox's CDN. + /// + /// Bypass the cache for long-running tasks. + /// The latest Roblox hash. + /// Thrown when the hash couldn't be fetched due to an external problem. + public static string GetLatestHash(bool bypassCache = false) + { + if (_latestHash is not null && !bypassCache) + return _latestHash; + + string version; + lock (Web) + { + version = Web.DownloadString("http://setup.rbxcdn.com/version.txt"); + } + + if (_latestHash is not null) + Log.Info($"GetLatestHash: Latest Roblox version: {version}"); + + return _latestHash = version.Split("version-")[1]; + } + + /// + /// Fetch the latest hash from Roblox's CDN. + /// + /// Bypass the cache for long-running tasks. + /// The latest Roblox hash. + /// Thrown when the hash couldn't be fetched due to an external problem. + public static async Task GetLatestHashAsync(bool bypassCache = false) + { + return await Task.Run(() => GetLatestHash(bypassCache)); + } + + /// + /// Install Roblox using the native installer. + /// + /// The installed Roblox client. + /// Thrown either when fetching the latest hash fails or the installer download fails. + /// Thrown when there was a problem accessing the filesystem. + /// The installer closed too early or the installation timed out. + public static Client NativeInstall() + { + // TODO: use a less crappy method for checking installer exit + var desktopShorctut = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), + "Roblox Player.lnk"); + if (File.Exists(desktopShorctut)) + File.Delete(desktopShorctut); + + var latestHash = GetLatestHash(); + var installPath = Path.Combine(InstallationPath, $"version-{latestHash}"); + var tempPath = Utility.GetTempDir(); + + var installerBin = Path.Combine(tempPath, "RobloxPlayerLauncher.exe"); + Log.Debug($"NativeInstall: Downloading installer to {installerBin}..."); + + lock (Web) + { + Web.DownloadFile($"https://setup.rbxcdn.com/version-{latestHash}-Roblox.exe", installerBin); + } + + Process setup; + try + { + setup = Process.Start(new ProcessStartInfo(installerBin) { WorkingDirectory = tempPath }); + } + catch (Win32Exception) + { + var ex = new PrematureCloseException(); + Log.Fatal("NativeInstall: Failed to start installer", ex); + throw ex; + } + + if (setup is null) + { + var ex = new PrematureCloseException(); + Log.Fatal("NativeInstall: Failed to start installer: Process is null", ex); + throw ex; + } + + IntPtr hWnd; + var waitStart = DateTime.Now; + while ((hWnd = setup.MainWindowHandle) == IntPtr.Zero) + { + Thread.Sleep(TimeSpan.FromSeconds(1.0d / 15)); + if (DateTime.Now - waitStart <= TimeSpan.FromSeconds(10)) + continue; + + var ex = new PrematureCloseException(); + Log.Fatal("NativeInstall: Installer unexpectedly closed", ex); + throw ex; + } + + ShowWindow(hWnd, SW_HIDE); // hide window to prevent user from seeing it + + // ReSharper enable PossibleNullReferenceException + + Log.Debug("NativeInstall: Waiting for installer to exit..."); + + waitStart = DateTime.Now; + while (!File.Exists(desktopShorctut)) + { + Thread.Sleep(TimeSpan.FromSeconds(1.0)); + + if (DateTime.Now - waitStart <= + TimeSpan.FromSeconds(120)) // Two minutes should be plenty. We also don't want CI to hang forever. + continue; + + var ex = new PrematureCloseException(); + Log.Fatal("NativeInstall: Installer timed out", ex); + throw ex; + } + + setup.Kill(); + Utility.WaitShare(Path.Combine(installPath, "RobloxPlayerBeta.exe"), FileShare.Read); + + if (Directory.Exists(installPath)) + { + Utility.WaitShare(installerBin, FileShare.Delete); + Thread.Sleep(100); + Directory.Delete(tempPath, true); + + Log.Debug("NativeInstall: Cleaned up."); + return new Client(installPath, latestHash); + } + + // if !installSuccess + { + var ex = new BadIntegrityException("Installer failed to execute."); + Log.Fatal("NativeInstall: Installer failed to execute", ex); + throw ex; + } + } + + /// + /// Install Roblox using the native installer. + /// + /// The installed Roblox client. + /// Thrown either when fetching the latest hash fails or the installer download fails. + /// Thrown when there was a problem accessing the filesystem. + /// The installer closed too early or the installation timed out. + public static async Task NativeInstallAsync() + { + return await Task.Run(NativeInstall); + } + + /// + /// Get a list of installed Roblox clients. + /// + /// A read-only list of Roblox clients. + /// Thrown when there was a problem accessing the filesystem. + public static IReadOnlyList GetClients() + { + List clients = new(); + + if (!Directory.Exists(InstallationPath)) // No valid installation of Roblox exists. + return clients; + + foreach (var item in Directory.EnumerateDirectories(InstallationPath)) + { + var dirName = Path.GetFileName(item); + + // Multiple threads could access this + lock (SkippedClients) + { + // I don't want this to error because someone was an idiot and put a random folder in the directory. + if (!dirName.StartsWith("version-")) + { + if (!SkippedClients.Contains(dirName)) + { + SkippedClients.Add(dirName); + Log.Warn($"GetClients: Skipping {dirName}: invalid directory name."); + } + + continue; + } + + if (!File.Exists(Path.Combine(item, "RobloxPlayerBeta.exe"))) + { + if (!SkippedClients.Contains(dirName)) + { + if (File.Exists(Path.Combine(item, "RobloxStudioBeta.exe"))) + Log.Warn($"GetClients: Skipping {Path.GetFileName(item)}: invalid installation."); + else + Log.Warn($"GetClients: Skipping {Path.GetFileName(item)}: invalid installation."); + } + + continue; + } + } + + var fileHash = Path.GetFileName(item).Split("version-")[1]; + clients.Add(new Client(item, fileHash)); + + Log.Debug($"Found {fileHash}."); + } + + return clients; + } + + /// + /// Get the installation manifest for a particular Roblox hash. + /// + /// The hash of Roblox to use. + /// The parsed manifest. + /// Thrown when fetching the manifest fails. + public static Manifest GetManifest(string hash) + { + try + { + string raw; + lock (Web) + { + raw = Web.DownloadString($"http://setup.rbxcdn.com/version-{hash}-rbxPkgManifest.txt"); + } + + return new Manifest(hash, raw); + } + catch (HttpRequestException ex) + { + Log.Error($"GetManifest: Failed to get manifest for version-{hash}. Is an invalid hash provided?", ex); + return null; + } + } + + /// + /// Get the installation manifest for a particular Roblox hash. + /// + /// The hash of Roblox to use. + /// The parsed manifest. + /// Thrown when fetching the manifest fails. + public static async Task GetManifestAsync(string hash) + { + return await Task.Run(() => GetManifest(hash)); + } + + /// + /// Install Roblox with an installation manifest. + /// + /// The installation manifest to refer to. + /// The installed class. + /// The hash of a downloaded file did not match. + /// Thrown when downloading a file fails. + /// Thrown when there was a problem accessing the filesystem. + public static Client Install(Manifest manifest) + { + if (!Directory.Exists(InstallationPath)) + { + Log.Info("Install: Running native installer for initial setup..."); + NativeInstall(); + + try + { + // Installer may have already installed the client. + return QueryClient(manifest.Hash); + } + catch (ClientNotFoundException) + { + } + } + + Log.Info($"Install: Preparing to install Roblox version-{manifest.Hash}..."); + var tempPath = Utility.GetTempDir(); + var path = Path.Combine(InstallationPath, $"version-{manifest.Hash}"); + Directory.CreateDirectory(path); + + // Download all files in parallel + Utility.DisperseActions(manifest.Files, file => + { + Log.Info($"Install: Downloading {file.Name}..."); + file.Download(tempPath); + }, 5); + + // Unzip all files + foreach (var file in manifest.Files) + { + var filePath = Path.Combine(tempPath, file.Name); + if (Path.GetExtension(filePath) == ".zip") + { + Log.Debug($"Install: Unzipping {file.Name}..."); + using (var archive = ZipFile.OpenRead(filePath)) + { + if (!ZipMap.TryGetValue(file.Name, out var extractPath)) + { + Log.Warn($"Install: No extraction path found for \"{file.Name}\"."); + goto ExtractFin; + } + + var extractTo = Path.Combine(path, extractPath); + if (!Directory.Exists(extractTo)) + Directory.CreateDirectory(extractTo); + + archive.ExtractToDirectory(extractTo, true); + } + + ExtractFin: + File.Delete(filePath); + } + else + { + Log.Warn($"Install: Skipped unknown file \"{file.Name}\"."); + } + } + + // No clue where the client got this. I just copied it and called it a day + File.WriteAllText(Path.Combine(path, "AppSettings.xml"), @" + + content + http://www.roblox.com + +"); + + // Clean up the temp directory + Directory.Delete(tempPath, true); + Log.Info("Install: Installation completed."); + + return new Client(path, manifest.Hash); + } + + /// + /// Install Roblox with an installation manifest. + /// + /// The installation manifest to refer to. + /// The installed class. + public static async Task InstallAsync(Manifest manifest) + { + return await Task.Run(() => Install(manifest)); + } + + /// + /// Install Roblox with a hash. + /// + /// The hash of Roblox to install. + /// The installed class. + public static Client Install(string hash = null) + { + if ((hash ??= GetLatestHash()) is null) + return null; + + var manifest = GetManifest(hash); + return manifest is not null ? Install(manifest) : null; + } + + /// + /// Install Roblox with a hash. + /// + /// The hash of Roblox to install. + /// The installed class. + public static async Task InstallAsync(string hash = null) + { + return await Task.Run(() => Install(hash)); + } + + /// + /// Uninstall Roblox with a class. + /// + /// The class to uninstall. + public static void Uninstall(Client client) + { + Directory.Delete(client.Location, true); + + var clients = GetClients(); + if (clients.Count < 1) + RemoveShortcuts(); + else + AddShortcuts(clients[0].Hash); + + Log.Debug($"Uninstall: Uninstalled Roblox version-{client.Hash}."); + } + + /// + /// Uninstall Roblox with a hash. + /// + /// The hash of Roblox to uninstall. + /// Thrown when the specified client doesn't exist. + public static void Uninstall(string hash) + { + Uninstall(QueryClient(hash ?? GetLatestHash())); + } + + /// + /// Uninstall Roblox with a hash. + /// + /// The hash of Roblox to uninstall. + /// Thrown when the specified client doesn't exist. + public static async Task UninstallAsync(string hash) + { + await Task.Run(() => Uninstall(hash)); + } + + internal static void AddShortcuts(string hash, string launcherBin = null) + { + RemoveShortcuts(); + + launcherBin ??= Path.Combine(InstallationPath, $"version-{hash}", "RobloxPlayerLauncher.exe"); + var workingDir = Path.GetDirectoryName(launcherBin); + + var menuShortcut = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "Microsoft\\Windows\\Start Menu\\Programs\\Roblox", "Roblox Player.lnk"); + var desktopShorctut = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), + "Roblox Player.lnk"); + + Utility.CreateShortcut(menuShortcut, launcherBin, workingDir); + Utility.CreateShortcut(desktopShorctut, launcherBin, workingDir); + + Log.Debug("AddShortcuts: Created shortcuts."); + } + + internal static void RemoveShortcuts() + { + var menuShortcut = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "Microsoft\\Windows\\Start Menu\\Programs\\Roblox", "Roblox Player.lnk"); + var desktopShorctut = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), + "Roblox Player.lnk"); + + if (File.Exists(menuShortcut)) + File.Delete(menuShortcut); + + if (File.Exists(desktopShorctut)) + File.Delete(desktopShorctut); + + Log.Debug("RemoveShortcuts: Removed shortcuts."); + } +} \ No newline at end of file diff --git a/src/Starlight/Bootstrap/Client.cs b/src/Starlight/Bootstrap/Client.cs new file mode 100644 index 0000000..8307ea4 --- /dev/null +++ b/src/Starlight/Bootstrap/Client.cs @@ -0,0 +1,21 @@ +using System.IO; + +namespace Starlight.Bootstrap; + +public class Client +{ + public readonly string Hash; + + public readonly string Launcher; + public readonly string Location; + + public readonly string Player; + + internal Client(string path, string hash) + { + Location = path; + Player = Path.Combine(path, "RobloxPlayerBeta.exe"); + Launcher = Path.Combine(path, "RobloxPlayerLauncher.exe"); + Hash = hash; + } +} \ No newline at end of file diff --git a/src/Starlight/Bootstrap/Downloadable.cs b/src/Starlight/Bootstrap/Downloadable.cs new file mode 100644 index 0000000..799c127 --- /dev/null +++ b/src/Starlight/Bootstrap/Downloadable.cs @@ -0,0 +1,74 @@ +using System; +using System.IO; +using System.Net.Http; +using System.Reflection; +using System.Security.Cryptography; +using System.Threading.Tasks; +using log4net; +using Starlight.Except; +using Starlight.Misc; + +namespace Starlight.Bootstrap; + +public class Downloadable +{ + // ReSharper disable once PossibleNullReferenceException + internal static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + + readonly Manifest _manifest; + + public readonly string Checksum; + + public readonly string Name; + + public readonly long Size; + + public readonly long TrueSize; + + internal Downloadable(Manifest manifest, string name, string checksum, long size, long trueSize) + { + _manifest = manifest; + Name = name; + Checksum = checksum; + TrueSize = trueSize; + Size = size; + } + + internal string Download(string dir) + { + var outPath = Path.Combine(dir, Name); + + using (var web = new HttpClient()) // Multithreading requires a separate client for each thread. + { + web.DownloadFile($"http://setup.rbxcdn.com/version-{_manifest.Hash}-{Name}", outPath); + } + + if (Check(dir)) + return outPath; + + var ex = new BadIntegrityException($"Downloaded file {Name} is corrupt!"); + Log.Fatal($"Download: Corrupt file: {Name}", ex); + throw ex; + } + + internal async Task DownloadAsync(string dir) + { + return await Task.Run(() => Download(dir)); + } + + internal bool Check(string dir) + { + var outPath = Path.Combine(dir, Name); + if (!File.Exists(outPath)) + return false; + + if (new FileInfo(outPath).Length != Size) + return false; + + using var stm = File.OpenRead(outPath); + using var hasher = MD5.Create(); + var hash = BitConverter.ToString(hasher.ComputeHash(stm)).Replace("-", "").ToLower(); + + return hash == Checksum; + } +} \ No newline at end of file diff --git a/src/Starlight/Bootstrap/Manifest.cs b/src/Starlight/Bootstrap/Manifest.cs new file mode 100644 index 0000000..240797c --- /dev/null +++ b/src/Starlight/Bootstrap/Manifest.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Linq; +using Starlight.Misc; + +namespace Starlight.Bootstrap; + +public class Manifest +{ + /// + /// The list of files contained in the manifest. + /// + public readonly IReadOnlyList Files; + + /// + /// The hash for the installation manifest. + /// + public readonly string Hash; + + internal Manifest(string hash, string raw) + { + List files = new(); + Hash = hash; + + // Parse manifest + var split = raw.Split("\r\n", "\n").Where(x => x != string.Empty).ToArray(); + for (var i = 1; i < split.Length;) + files.Add(new Downloadable(this, split[i++], split[i++], long.Parse(split[i++]), long.Parse(split[i++]))); + + Files = files; + } +} \ No newline at end of file diff --git a/src/Starlight/Core/BootstrapException.cs b/src/Starlight/Core/BootstrapException.cs deleted file mode 100644 index 4febf5e..0000000 --- a/src/Starlight/Core/BootstrapException.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; - -namespace Starlight.Core -{ - public class BootstrapException : Exception - { - public BootstrapException(string message) : base(message) { } - public BootstrapException(string message, Exception inner) : base(message, inner) { } - } -} diff --git a/src/Starlight/Core/Bootstrapper.cs b/src/Starlight/Core/Bootstrapper.cs deleted file mode 100644 index 92dfc27..0000000 --- a/src/Starlight/Core/Bootstrapper.cs +++ /dev/null @@ -1,397 +0,0 @@ -using log4net; -using Starlight.Misc; -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Diagnostics; -using System.IO; -using System.IO.Compression; -using System.Linq; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using System.Web; -using static Starlight.Misc.Shared; -using static Starlight.Misc.Native; - -namespace Starlight.Core -{ - public class Bootstrapper - { - // ReSharper disable once PossibleNullReferenceException - internal static readonly ILog Log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); - - static readonly IReadOnlyDictionary ZipMap = new Dictionary - { - { "content-avatar.zip", "content\\avatar" }, - { "content-configs.zip", "content\\configs" }, - { "content-fonts.zip", "content\\fonts" }, - { "content-models.zip", "content\\models" }, - { "content-platform-fonts.zip", "PlatformContent\\pc\\fonts" }, - { "content-sky.zip", "content\\sky" }, - { "content-sounds.zip", "content\\sounds" }, - { "content-terrain.zip", "PlatformContent\\pc\\terrain" }, - { "content-textures2.zip", "content\\textures" }, - { "content-textures3.zip", "PlatformContent\\pc\\textures" }, - { "extracontent-luapackages.zip", "ExtraContent\\LuaPackages" }, - { "extracontent-models.zip", "ExtraContent\\models" }, - { "extracontent-places.zip", "ExtraContent\\places" }, - { "extracontent-textures.zip", "ExtraContent\\textures" }, - { "extracontent-translations.zip", "ExtraContent\\translations" }, - { "RobloxApp.zip", "." }, - { "shaders.zip", "shaders" }, - { "ssl.zip", "ssl" }, - }; - - public static Client QueryClient(string hash) - { - var client = GetClients().FirstOrDefault(x => x.Hash == hash); - if (client is null) - Log.Warn($"QueryClient {hash}: Failed to find client"); - else - Log.Debug($"QueryClient {hash}: Success"); - return client; - } - - static string _latestHash; // Cache for a micro-optimization - public static string GetLatestHash() - { - if (_latestHash is not null) - return _latestHash; - - try - { - string version; - lock (Web) - version = Web.DownloadString("http://setup.rbxcdn.com/version.txt"); - Log.Info($"GetLatestHash: Latest Roblox version: {version}"); - return _latestHash = version.Split("version-")[1]; - } - catch (HttpException innerEx) - { - var ex = new BootstrapException("Failed to get latest hash.", innerEx); - Log.Fatal("GetLatestHash: Failed to get latest hash", ex); - throw ex; - } - } - - public static async Task GetLatestHashAsync() => - await Task.Run(GetLatestHash); - - public static Client NativeInstall() - { - // TODO: use a less crappy method for checking installer exit - var desktopShorctut = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "Roblox Player.lnk"); - if (File.Exists(desktopShorctut)) - File.Delete(desktopShorctut); - - var latestHash = GetLatestHash(); - var installPath = Path.Combine(GetInstallationPath(), $"version-{latestHash}"); - var tempPath = Utility.GetTempDir(); - - var installerBin = Path.Combine(tempPath, "RobloxPlayerLauncher.exe"); - Log.Debug($"NativeInstall: Downloading installer to {installerBin}..."); - try - { - lock (Web) - Web.DownloadFile($"https://setup.rbxcdn.com/version-{latestHash}-Roblox.exe", installerBin); - } - catch (HttpException innerEx) - { - var ex = new BootstrapException("Failed to download installer.", innerEx); - Log.Fatal("NativeInstall: Failed to download installer", ex); - throw ex; - } - - Process setup; - try - { - setup = Process.Start(new ProcessStartInfo(installerBin) { WorkingDirectory = tempPath }); - } - catch (Win32Exception innerEx) - { - var ex = new BootstrapException("Failed to start installer.", innerEx); - Log.Fatal("NativeInstall: Failed to start installer", ex); - throw ex; - } - - if (setup is null) - { - var ex = new BootstrapException("Failed to start installer: Process is null."); - Log.Fatal("NativeInstall: Failed to start installer: Process is null", ex); - throw ex; - } - - IntPtr hWnd; - var waitStart = DateTime.Now; - while ((hWnd = setup.MainWindowHandle) == IntPtr.Zero) - { - Thread.Sleep(TimeSpan.FromSeconds(1.0d / 15)); - if (DateTime.Now - waitStart <= TimeSpan.FromSeconds(10)) - continue; - - var ex = new BootstrapException("Installer unexpectedly closed."); - Log.Fatal("NativeInstall: Installer unexpectedly closed", ex); - throw ex; - } - ShowWindow(hWnd, SW_HIDE); // hide window to prevent user from seeing it - - Log.Debug("NativeInstall: Waiting for installer to exit..."); - - waitStart = DateTime.Now; - while (!File.Exists(desktopShorctut)) - { - Thread.Sleep(TimeSpan.FromSeconds(1.0)); - - if (DateTime.Now - waitStart <= TimeSpan.FromSeconds(120)) // Two minutes should be plenty. We also don't want CI to hang forever. - continue; - - var ex = new BootstrapException("Installer timed out."); - Log.Fatal("NativeInstall: Installer timed out", ex); - throw ex; - } - - setup.Kill(); - Thread.Sleep(2000); - - if (Directory.Exists(installPath)) - { - Directory.Delete(tempPath, true); - Log.Debug("NativeInstall: Cleaned up."); - return new Client(installPath, latestHash); - } - - // if !installSuccess - { - var ex = new BootstrapException("Installer failed to execute."); - Log.Fatal("NativeInstall: Installer failed to execute", ex); - throw ex; - } - } - - public static async Task NativeInstallAsync() => - await Task.Run(NativeInstall); - - static volatile bool _pathDoesntExist; - internal static string GetInstallationPath() - { - var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); - var installPath = Path.Combine(localAppData, "Roblox\\Versions"); - - if (Directory.Exists(installPath)) - return installPath; - - if (_pathDoesntExist) - return installPath; - _pathDoesntExist = true; - - Log.Warn("GetInstallationPath: Installation path doesn't exist"); - return installPath; - } - - static readonly List SkippedClients = new(); - public static IReadOnlyList GetClients() - { - List clients = new(); - - var path = GetInstallationPath(); - if (!Directory.Exists(path)) // No valid installation of Roblox exists. - return clients; - - foreach (var item in Directory.EnumerateDirectories(path)) - { - var dirName = Path.GetFileName(item); - - // Multiple threads could access this - lock (SkippedClients) - { - // I don't want this to error because someone was an idiot and put a random folder in the directory. - if (!dirName.StartsWith("version-")) - { - if (!SkippedClients.Contains(dirName)) - { - SkippedClients.Add(dirName); - Log.Warn($"GetClients: Skipping {dirName}: invalid directory name."); - } - continue; - } - - if (!File.Exists(Path.Combine(item, "RobloxPlayerBeta.exe"))) - { - if (!SkippedClients.Contains(dirName)) - { - if (File.Exists(Path.Combine(item, "RobloxStudioBeta.exe"))) - Log.Warn($"GetClients: Skipping {Path.GetFileName(item)}: invalid installation."); - else - Log.Warn($"GetClients: Skipping {Path.GetFileName(item)}: invalid installation."); - } - continue; - } - } - - var fileHash = Path.GetFileName(item).Split("version-")[1]; - clients.Add(new Client(item, fileHash)); - - Log.Debug($"Found {fileHash}."); - } - - return clients; - } - - public static Manifest GetManifest(string hash) - { - try - { - string raw; - lock (Web) - raw = Web.DownloadString($"http://setup.rbxcdn.com/version-{hash}-rbxPkgManifest.txt"); - return new Manifest(hash, raw); - } - catch (HttpRequestException ex) - { - Log.Error($"GetManifest: Failed to get manifest for version-{hash}. Is an invalid hash provided?", ex); - return null; - } - } - - public static async Task GetManifestAsync(string hash) => - await Task.Run(() => GetManifest(hash)); - - public static Client Install(Manifest manifest) - { - var installationPath = GetInstallationPath(); - if (!Directory.Exists(installationPath)) - { - Log.Info("Install: Running native installer for initial setup..."); - NativeInstall(); - var client = QueryClient(manifest.Hash); - if (client is not null) // Installer may have already installed the client. - return client; - } - - Log.Info($"Install: Preparing to install Roblox version-{manifest.Hash}..."); - var tempPath = Utility.GetTempDir(); - var path = Path.Combine(installationPath, $"version-{manifest.Hash}"); - Directory.CreateDirectory(path); - - // Download all files in parallel - Utility.DisperseActions(manifest.Files, file => - { - Log.Info($"Install: Downloading {file.Name}..."); - file.Download(tempPath); - }, 5); - - // Unzip all files - foreach (var file in manifest.Files) - { - var filePath = Path.Combine(tempPath, file.Name); - if (Path.GetExtension(filePath) == ".zip") - { - Log.Debug($"Install: Unzipping {file.Name}..."); - using (var archive = ZipFile.OpenRead(filePath)) - { - if (!ZipMap.TryGetValue(file.Name, out var extractPath)) - { - Log.Warn($"Install: No extraction path found for \"{file.Name}\"."); - goto ExtractFin; - } - - var extractTo = Path.Combine(path, extractPath); - if (!Directory.Exists(extractTo)) - Directory.CreateDirectory(extractTo); - - archive.ExtractToDirectory(extractTo, true); - } - - ExtractFin: - File.Delete(filePath); - } - else - Log.Warn($"Install: Skipped unknown file \"{file.Name}\"."); - } - - // No clue where the client got this. I just copied it and called it a day - File.WriteAllText(Path.Combine(path, "AppSettings.xml"), @" - - content - http://www.roblox.com - -"); - - // Clean up the temp directory - Directory.Delete(tempPath, true); - Log.Info("Install: Installation completed."); - - return new Client(path, manifest.Hash); - } - - public static async Task InstallAsync(Manifest manifest) => - await Task.Run(() => Install(manifest)); - - public static Client Install(string hash = null) - { - hash ??= GetLatestHash(); - - var manifest = GetManifest(hash); - if (manifest is not null) - return Install(manifest); - - var ex = new BootstrapException("Failed to get manifest."); - Log.Fatal($"Install: Failed to get manifest for version-{hash}", ex); - throw ex; - } - - public static async Task InstallAsync(string hash = null) => - await Task.Run(() => Install(hash)); - - public static void Uninstall(string hash = null) - { - hash ??= GetLatestHash(); - - var path = Path.Combine(GetInstallationPath(), $"version-{hash}"); - if (Directory.Exists(path)) - Directory.Delete(path, true); - - var clients = GetClients(); - if (clients.Count < 1) - RemoveShortcuts(); - else - AddShortcuts(clients[0].Hash); - - Log.Debug($"Uninstall: Uninstalled Roblox version-{hash}."); - } - - public static void Uninstall(Client client) => - Uninstall(client.Hash); - - internal static void AddShortcuts(string hash, string launcher = null) - { - RemoveShortcuts(); - - var roblox = Path.Combine(GetInstallationPath(), $"version-{hash}"); - launcher ??= Path.Combine(roblox, "RobloxPlayerLauncher.exe"); - - var menuShortcut = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Microsoft\\Windows\\Start Menu\\Programs\\Roblox", "Roblox Player.lnk"); - var desktopShorctut = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "Roblox Player.lnk"); - - Utility.CreateShortcut(menuShortcut, launcher, roblox); - Utility.CreateShortcut(desktopShorctut, launcher, roblox); - - Log.Debug("AddShortcuts: Created shortcuts."); - } - - internal static void RemoveShortcuts() - { - var menuShortcut = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Microsoft\\Windows\\Start Menu\\Programs\\Roblox", "Roblox Player.lnk"); - var desktopShorctut = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "Roblox Player.lnk"); - - if (File.Exists(menuShortcut)) - File.Delete(menuShortcut); - - if (File.Exists(desktopShorctut)) - File.Delete(desktopShorctut); - - Log.Debug("RemoveShortcuts: Removed shortcuts."); - } - } -} diff --git a/src/Starlight/Core/Client.cs b/src/Starlight/Core/Client.cs deleted file mode 100644 index 385d22f..0000000 --- a/src/Starlight/Core/Client.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace Starlight.Core -{ - public class Client - { - public readonly string Directory; - - public readonly string Path; - - public readonly string LauncherPath; - - public readonly string Hash; - - internal Client(string path, string hash) - { - Directory = path; - Path = System.IO.Path.Combine(path, "RobloxPlayerBeta.exe"); - LauncherPath = System.IO.Path.Combine(path, "RobloxPlayerLauncher.exe"); - Hash = hash; - } - } -} diff --git a/src/Starlight/Core/Downloadable.cs b/src/Starlight/Core/Downloadable.cs deleted file mode 100644 index 9f3a69a..0000000 --- a/src/Starlight/Core/Downloadable.cs +++ /dev/null @@ -1,69 +0,0 @@ -using log4net; -using Starlight.Misc; -using System; -using System.IO; -using System.Net.Http; -using System.Security.Cryptography; -using System.Threading.Tasks; - -namespace Starlight.Core -{ - public class Downloadable - { - // ReSharper disable once PossibleNullReferenceException - internal static readonly ILog Log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); - - readonly Manifest _manifest; - - public readonly string Name; - - public readonly string Checksum; - - public readonly long Size; - - public readonly long TrueSize; - - internal Downloadable(Manifest manifest, string name, string checksum, long size, long trueSize) - { - _manifest = manifest; - Name = name; - Checksum = checksum; - TrueSize = trueSize; - Size = size; - } - - internal string Download(string dir) - { - var outPath = Path.Combine(dir, Name); - - using (var web = new HttpClient()) // Multithreading requires a separate client for each thread. - web.DownloadFile($"http://setup.rbxcdn.com/version-{_manifest.Hash}-{Name}", outPath); - - if (Check(dir)) - return outPath; - - var ex = new BootstrapException($"Downloaded file {Name} is corrupt!"); - Log.Fatal($"Download: Corrupt file: {Name}", ex); - throw ex; - } - - internal async Task DownloadAsync(string dir) => - await Task.Run(() => Download(dir)); - - internal bool Check(string dir) - { - var outPath = Path.Combine(dir, Name); - if (!File.Exists(outPath)) - return false; - - if (new FileInfo(outPath).Length != Size) - return false; - - using var stm = File.OpenRead(outPath); - using var hasher = MD5.Create(); - var hash = BitConverter.ToString(hasher.ComputeHash(stm)).Replace("-", "").ToLower(); - - return hash == Checksum; - } - } -} diff --git a/src/Starlight/Core/IRobloxLaunchParams.cs b/src/Starlight/Core/IRobloxLaunchParams.cs deleted file mode 100644 index c9f5a56..0000000 --- a/src/Starlight/Core/IRobloxLaunchParams.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Globalization; - -namespace Starlight.Core -{ - public interface IRobloxLaunchParams - { - public string Ticket { get; set; } - - public DateTimeOffset LaunchTime { get; set; } - - public Rbx.JoinGame.JoinRequest Request { get; set; } - - public CultureInfo RobloxLocale { get; set; } - - public CultureInfo GameLocale { get; set; } - } -} diff --git a/src/Starlight/Core/IStarlightLaunchParams.cs b/src/Starlight/Core/IStarlightLaunchParams.cs deleted file mode 100644 index cc1d9ba..0000000 --- a/src/Starlight/Core/IStarlightLaunchParams.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Starlight.Core -{ - public interface IStarlightLaunchParams - { - public int FpsCap { get; set; } - - public bool Headless { get; set; } - - public bool Spoof { get; set; } - - public string Hash { get; set; } - - public string Resolution { get; set; } - } -} diff --git a/src/Starlight/Core/LaunchException.cs b/src/Starlight/Core/LaunchException.cs deleted file mode 100644 index 0c416ed..0000000 --- a/src/Starlight/Core/LaunchException.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; - -namespace Starlight.Core -{ - public class LaunchException : Exception - { - public LaunchException(string message) : base(message) { } - } -} diff --git a/src/Starlight/Core/LaunchParams.cs b/src/Starlight/Core/LaunchParams.cs deleted file mode 100644 index fa15ebc..0000000 --- a/src/Starlight/Core/LaunchParams.cs +++ /dev/null @@ -1,71 +0,0 @@ -using Starlight.Rbx.JoinGame; -using System; -using System.Globalization; -using System.Text; - -namespace Starlight.Core -{ - public class LaunchParams : IRobloxLaunchParams, IStarlightLaunchParams - { - public int FpsCap { get; set; } - - public bool Headless { get; set; } - - public bool Spoof { get; set; } - - public string Hash { get; set; } - - public string Ticket { get; set; } - - public DateTimeOffset LaunchTime { get; set; } = DateTime.Now; - - public string Resolution { get; set; } - - JoinRequest _request; - public JoinRequest Request - { - get => _request; - set - { - if (TrackerId is not null) - value.BrowserTrackerId = TrackerId; - _request = value; - } - } - - public long? TrackerId - { - get => Request?.BrowserTrackerId; - set => Request.BrowserTrackerId = value; - } - - public CultureInfo RobloxLocale { get; set; } = CultureInfo.CurrentCulture; - - public CultureInfo GameLocale { get; set; } = CultureInfo.CurrentCulture; - - public void Merge(T args) - { - foreach (var member in typeof(T).GetProperties()) - { - var value = member.GetValue(args); - member.SetValue(this, value); - } - } - - public override string ToString() - { - var sb = new StringBuilder(); - - sb.Append("Ticket=\"REDACTED-FOR-PRIVACY\";"); // you're welcome :) - foreach (var prop in GetType().GetProperties()) - { - if (!prop.CanRead || prop.Name is "Ticket" or "Request") - continue; - - sb.Append(prop.Name + $"=\"{prop.GetValue(this)}\";"); - } - - return sb.ToString(); - } - } -} diff --git a/src/Starlight/Core/Launcher.cs b/src/Starlight/Core/Launcher.cs deleted file mode 100644 index ef9b09c..0000000 --- a/src/Starlight/Core/Launcher.cs +++ /dev/null @@ -1,167 +0,0 @@ -using log4net; -using Starlight.Misc; -using Starlight.RbxApp; -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using System.Windows.Forms; -using static Starlight.Misc.Native; - -namespace Starlight.Core -{ - public class Launcher - { - // ReSharper disable once PossibleNullReferenceException - internal static readonly ILog Log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); - - /* Singleton */ - - static Mutex _rbxSingleton; - - public static void CommitSingleton() => - _rbxSingleton = new Mutex(true, "ROBLOX_singletonMutex"); - - public static void ReleaseSingleton() => // Releases on thread exit as well. - _rbxSingleton.Dispose(); - - /* Launcher */ - - public static RbxInstance Launch(LaunchParams info, IStarlightLaunchParams extras = null) - { - if (extras != null) - info.Merge(extras); - - if (string.IsNullOrWhiteSpace(info.Hash)) - info.Hash = Bootstrapper.GetLatestHash(); - - Log.Info($"Launch: Preparing to launch Roblox version-{info.Hash}..."); - - // Spoof the trackers if spoofing is enabled - if (info.Spoof) - { - info.TrackerId = Utility.SecureRandomInteger(); - info.LaunchTime = DateTime.Now; - Log.Debug($"Launch: Spoofed BrowserTrackerId to {info.TrackerId} and LaunchTime to {info.LaunchTime}"); - } - - // Get client - Log.Debug($"Launch: Querying {info.Hash}..."); - var client = Bootstrapper.QueryClient(info.Hash); - if (client is null) - { - var ex = new LaunchException("No valid client corresponds the given hash."); - Log.Fatal("Launch: Client does not exist.", ex); - throw ex; - } - - // Open Roblox client - Log.Debug("Launch: Native open..."); - if (!OpenRoblox(client.Path, info, out var procInfo)) - { - var ex = new LaunchException("Failed to open Roblox application."); - Log.Fatal("Launch: Failed to open Roblox.", ex); - throw ex; - } - ResumeThread(procInfo.hThread); - - RbxInstance inst; - try - { - inst = new RbxInstance(procInfo.dwProcessId); - } - catch - { - var ex = new LaunchException("Failed to initialize Roblox instance."); - Log.Fatal("Launch: Failed to initialize Roblox instance. Roblox may have prematurely exited.", ex); - throw ex; - } - - // Wait for Roblox's window to open - Log.Debug("Launch: Waiting for Roblox window..."); - - IntPtr hWnd; - var waitStart = DateTime.Now; - while ((hWnd = inst.Proc.MainWindowHandle) == IntPtr.Zero) - { - Thread.Sleep(TimeSpan.FromSeconds(1.0d / 15)); - if (DateTime.Now - waitStart <= TimeSpan.FromSeconds(10)) - continue; - - var ex = new LaunchException("Roblox unexpectedly closed."); - Log.Fatal("Launch: Roblox unexpectedly closed.", ex); - throw ex; - } - - Log.Debug("Roblox launched!"); - - /* Runtime */ - - // Set FPS cap - // TODO: Disable rendering by hooking render job instead of limiting fps, will be more efficient. - if (info.FpsCap != 0) - { - inst.SetFrameDelay(1.0d / info.FpsCap); - Log.Debug($"Launch: Set FPS cap to {info.FpsCap}."); - } - - // Set resolution of Roblox - if (!string.IsNullOrWhiteSpace(info.Resolution)) - { - var res = Utility.ParseResolution(info.Resolution); - if (res.HasValue) - { - var bounds = Utility.GetWindowBounds(hWnd); - var screenBounds = Screen.PrimaryScreen.WorkingArea with { X = 0, Y = 0 }; - - SetWindowPos( - hWnd, - IntPtr.Zero, - screenBounds.Right / 2 - bounds.Width / 2, // Center X - screenBounds.Bottom / 2 - bounds.Height / 2, // Center Y - res.Value.Item1, - res.Value.Item2, - SWP_NOOWNERZORDER | SWP_NOZORDER); - SetWindowLong(hWnd, GWL_STYLE, WS_POPUPWINDOW); // Remove window styles (title bar, etc.) - ShowWindow(hWnd, SW_SHOW); - - Log.Debug($"Launch: Set resolution to {info.Resolution}."); - } - else - Log.Error("Launch: Skipped setting resolution because parse failed."); - } - - // Enter headless mode - if (info.Headless) - { - ShowWindow(hWnd, SW_HIDE); - Log.Debug("Launch: Roblox window hidden. Client is now headless."); - } - - Log.Debug("Launch: Finished post-launch."); - Log.Info("Launch: Roblox launched."); - - return inst; - } - - public static async Task LaunchAsync(LaunchParams info, IStarlightLaunchParams extras = null) => - await Task.Run(() => Launch(info, extras)); - - internal static bool OpenRoblox(string robloxPath, LaunchParams info, out PROCESS_INFORMATION procInfo) - { - STARTUPINFO startInfo = new(); - return CreateProcess( - Path.GetFullPath(robloxPath), - $"--play -a https://auth.roblox.com/v1/authentication-ticket/redeem -t {info.Ticket} -j {info.Request.Serialize()} -b {info.TrackerId} " + // Updated to another endpoint. - $"--launchtime={info.LaunchTime.ToUnixTimeMilliseconds()} --rloc {info.RobloxLocale.Name} --gloc {info.GameLocale.Name}", - 0, - 0, - false, - ProcessCreationFlags.CREATE_SUSPENDED, - 0, - null, - ref startInfo, - out procInfo); - } - } -} diff --git a/src/Starlight/Core/Manifest.cs b/src/Starlight/Core/Manifest.cs deleted file mode 100644 index 6f9ffd1..0000000 --- a/src/Starlight/Core/Manifest.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Starlight.Misc; -using System.Collections.Generic; -using System.Linq; - -namespace Starlight.Core -{ - public class Manifest - { - public readonly IReadOnlyList Files; - - public readonly string Hash; - - internal Manifest(string hash, string raw) - { - List files = new(); - Hash = hash; - - // Parse manifest - var split = raw.Split("\r\n", "\n").Where(x => x != string.Empty).ToArray(); - for (var i = 1; i < split.Length;) - files.Add(new Downloadable(this, split[i++], split[i++], long.Parse(split[i++]), long.Parse(split[i++]))); - - Files = files; - } - } -} diff --git a/src/Starlight/Core/Scheme.cs b/src/Starlight/Core/Scheme.cs deleted file mode 100644 index cc51573..0000000 --- a/src/Starlight/Core/Scheme.cs +++ /dev/null @@ -1,176 +0,0 @@ -using log4net; -using Microsoft.Win32; -using Starlight.Misc; -using Starlight.Rbx.JoinGame; -using Starlight.RbxApp; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using System.Web; - -namespace Starlight.Core -{ - [Flags] - enum ParseResultFlags - { - TicketExists = 0x0, - RequestExists = 0x2, - RequestParsed = 0x4, - LaunchTimeExists = 0x8, - LaunchTimeParsed = 0x10, - TrackerIdExists = 0x20, - TrackerIdParsed = 0x40, - RobloxLocaleExists = 0x80, - RobloxLocaleParsed = 0x100, - GameLocaleExists = 0x200, - GameLocaleParsed = 0x300, - Success = TicketExists | RequestExists | RequestParsed | LaunchTimeExists - | LaunchTimeParsed | TrackerIdExists | TrackerIdParsed | RobloxLocaleExists - | RobloxLocaleParsed | GameLocaleExists | GameLocaleParsed - } - - public class Scheme - { - // ReSharper disable once PossibleNullReferenceException - internal static readonly ILog Log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); - - internal static IReadOnlyDictionary ParseRaw(string payload) - { - try - { - var split = HttpUtility.UrlDecode(payload).Split(' '); - return split.Select(t => t.Split(':')).ToDictionary(pair => pair[0], pair => string.Join(":", pair.Skip(1))); - } - catch - { - Log.Error("Failed to parse raw payload data."); - return null; - } - } - - public static LaunchParams Parse(string rawArgs) - { - LaunchParams info = new(); - var args = ParseRaw(rawArgs); - ParseResultFlags result = 0; - - if (args.TryGetValue("gameinfo", out var ticket)) - { - result |= ParseResultFlags.TicketExists; - info.Ticket = ticket; - } - - if (args.TryGetValue("placelauncherurl", out var launchUrl)) - { - result |= ParseResultFlags.RequestExists; - if (Uri.TryCreate(launchUrl, UriKind.Absolute, out var launchUri)) - { - result |= ParseResultFlags.RequestParsed; - info.Request = new JoinRequest(launchUri); - } - } - - if (args.TryGetValue("launchtime", out var launchTimeStr)) - { - result |= ParseResultFlags.LaunchTimeExists; - if (long.TryParse(launchTimeStr, out var launchTime)) - { - result |= ParseResultFlags.LaunchTimeParsed; - info.LaunchTime = DateTimeOffset.FromUnixTimeMilliseconds(launchTime); - } - } - - if (args.TryGetValue("browsertrackerid", out var trackerIdStr)) - { - result |= ParseResultFlags.TrackerIdExists; - if (long.TryParse(trackerIdStr, out var trackerId)) - { - result |= ParseResultFlags.TrackerIdParsed; - info.TrackerId = trackerId; - } - } - - if (args.TryGetValue("robloxLocale", out var rbxLocaleStr)) - { - result |= ParseResultFlags.RobloxLocaleExists; - if (Utility.TryGetCultureInfo(rbxLocaleStr, out var rbxLocale)) - { - result |= ParseResultFlags.RobloxLocaleParsed; - info.RobloxLocale = rbxLocale; - } - } - - if (args.TryGetValue("gameLocale", out var gameLocaleStr)) - { - result |= ParseResultFlags.GameLocaleExists; - if (Utility.TryGetCultureInfo(gameLocaleStr, out var gameLocale)) - { - result |= ParseResultFlags.GameLocaleParsed; - info.GameLocale = gameLocale; - } - } - - if (result.HasFlag(ParseResultFlags.Success)) - return info; - - Log.Error($"Failed to parse scheme payload. Parse result: {result}"); - return null; - } - - public static async Task LaunchAsync(string args, IStarlightLaunchParams extras = null) - { - var parsed = Parse(args); - - if (parsed == null) - { - var ex = new SchemeParseException("Failed to parse scheme payload."); - Log.Fatal("Failed to deserialize payload into LaunchParams.", ex); - throw ex; - } - - return await Launcher.LaunchAsync(parsed, extras); - } - - public static RbxInstance Launch(string args, IStarlightLaunchParams extras = null) => - LaunchAsync(args, extras).Result; - - public static bool Hook(string launcherBin, string options = "") - { - try - { - using var registryKey = Registry.CurrentUser.CreateSubKey("Software\\Classes\\roblox-player\\shell\\open\\command"); - registryKey?.SetValue(string.Empty, $"\"{launcherBin}\" {options}%1", RegistryValueKind.String); - return true; - } - catch - { - Log.Error("Failed to hook scheme."); - return false; - } - } - - public static bool Unhook() - { - var clients = Bootstrapper.GetClients(); - if (clients.Count < 1) - { - Bootstrapper.RemoveShortcuts(); - return true; - } - - var rbxBin = clients.First().LauncherPath; - try - { - using var registryKey = Registry.CurrentUser.CreateSubKey("Software\\Classes\\roblox-player\\shell\\open\\command"); - registryKey?.SetValue(string.Empty, $"\"{rbxBin}\" %1", RegistryValueKind.String); - return true; - } - catch - { - Log.Error("Failed to unhook scheme."); - return false; - } - } - } -} diff --git a/src/Starlight/Core/SchemeParseException.cs b/src/Starlight/Core/SchemeParseException.cs deleted file mode 100644 index 58e42fb..0000000 --- a/src/Starlight/Core/SchemeParseException.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; - -namespace Starlight.Core -{ - public class SchemeParseException : Exception - { - public SchemeParseException(string message) : base(message) { } - } -} diff --git a/src/Starlight/Except/BadIntegrityException.cs b/src/Starlight/Except/BadIntegrityException.cs new file mode 100644 index 0000000..90d8a37 --- /dev/null +++ b/src/Starlight/Except/BadIntegrityException.cs @@ -0,0 +1,13 @@ +using System; + +namespace Starlight.Except; + +/// +/// Thrown when a file's integrity is compromised. This is usually caused by file corruption. +/// +public sealed class BadIntegrityException : Exception +{ + public BadIntegrityException(string message) : base(message) + { + } +} \ No newline at end of file diff --git a/src/Starlight/Except/ClientNotFoundException.cs b/src/Starlight/Except/ClientNotFoundException.cs new file mode 100644 index 0000000..fa09a1e --- /dev/null +++ b/src/Starlight/Except/ClientNotFoundException.cs @@ -0,0 +1,15 @@ +using System; +using Starlight.Bootstrap; + +namespace Starlight.Except; + +/// +/// Thrown when is unable to find a client with the specified hash. +/// +public sealed class ClientNotFoundException : Exception +{ + internal ClientNotFoundException(string hash) + { + Data.Add("Hash", hash); + } +} \ No newline at end of file diff --git a/src/Starlight/Except/PostLaunchException.cs b/src/Starlight/Except/PostLaunchException.cs new file mode 100644 index 0000000..927baab --- /dev/null +++ b/src/Starlight/Except/PostLaunchException.cs @@ -0,0 +1,10 @@ +using System; + +namespace Starlight.Except; + +public class PostLaunchException : Exception +{ + public PostLaunchException(string message) : base(message) + { + } +} \ No newline at end of file diff --git a/src/Starlight/Except/PrematureCloseException.cs b/src/Starlight/Except/PrematureCloseException.cs new file mode 100644 index 0000000..389c6f7 --- /dev/null +++ b/src/Starlight/Except/PrematureCloseException.cs @@ -0,0 +1,10 @@ +using System; + +namespace Starlight.Except; + +/// +/// Thrown when a native process used by Starlight prematurely closes. +/// +public sealed class PrematureCloseException : Exception +{ +} \ No newline at end of file diff --git a/src/Starlight/Except/SchemeParseException.cs b/src/Starlight/Except/SchemeParseException.cs new file mode 100644 index 0000000..ccc53e1 --- /dev/null +++ b/src/Starlight/Except/SchemeParseException.cs @@ -0,0 +1,10 @@ +using System; + +namespace Starlight.Except; + +public class SchemeParseException : Exception +{ + public SchemeParseException(string message) : base(message) + { + } +} \ No newline at end of file diff --git a/src/Starlight/FodyWeavers.xml b/src/Starlight/FodyWeavers.xml index 51bb2c0..e6fa1f6 100644 --- a/src/Starlight/FodyWeavers.xml +++ b/src/Starlight/FodyWeavers.xml @@ -1,4 +1,5 @@  + diff --git a/src/Starlight/Launch/IRobloxLaunchParams.cs b/src/Starlight/Launch/IRobloxLaunchParams.cs new file mode 100644 index 0000000..f6dae58 --- /dev/null +++ b/src/Starlight/Launch/IRobloxLaunchParams.cs @@ -0,0 +1,18 @@ +using System; +using System.Globalization; +using Starlight.Apis.JoinGame; + +namespace Starlight.Launch; + +public interface IRobloxLaunchParams +{ + public string Ticket { get; set; } + + public DateTimeOffset LaunchTime { get; set; } + + public JoinRequest Request { get; set; } + + public CultureInfo RobloxLocale { get; set; } + + public CultureInfo GameLocale { get; set; } +} \ No newline at end of file diff --git a/src/Starlight/Launch/IStarlightLaunchParams.cs b/src/Starlight/Launch/IStarlightLaunchParams.cs new file mode 100644 index 0000000..d5f0c4d --- /dev/null +++ b/src/Starlight/Launch/IStarlightLaunchParams.cs @@ -0,0 +1,18 @@ +using Starlight.PostLaunch; + +namespace Starlight.Launch; + +public interface IStarlightLaunchParams +{ + public int FpsCap { get; set; } + + public bool Headless { get; set; } + + public bool Spoof { get; set; } + + public string Hash { get; set; } + + public string Resolution { get; set; } + + public AttachMethod AttachMethod { get; set; } +} \ No newline at end of file diff --git a/src/Starlight/Launch/LaunchParams.cs b/src/Starlight/Launch/LaunchParams.cs new file mode 100644 index 0000000..7e71f29 --- /dev/null +++ b/src/Starlight/Launch/LaunchParams.cs @@ -0,0 +1,80 @@ +using System; +using System.Globalization; +using System.Text; +using Starlight.Apis.JoinGame; +using Starlight.PostLaunch; + +namespace Starlight.Launch; + +public class LaunchParams : IRobloxLaunchParams, IStarlightLaunchParams +{ + JoinRequest _request; + + public long? TrackerId + { + get => Request?.BrowserTrackerId; + set => Request.BrowserTrackerId = value; + } + + public CultureInfo RobloxLocale { get; set; } = CultureInfo.CurrentCulture; + + public CultureInfo GameLocale { get; set; } = CultureInfo.CurrentCulture; + + public string Ticket { get; set; } + + public DateTimeOffset LaunchTime { get; set; } = DateTime.Now; + + public JoinRequest Request + { + get => _request; + set + { + if (TrackerId is not null) + value.BrowserTrackerId = TrackerId; + _request = value; + } + } + + public int FpsCap { get; set; } + + public bool Headless { get; set; } + + public bool Spoof { get; set; } + + public string Hash { get; set; } + + public string Resolution { get; set; } + + public AttachMethod AttachMethod { get; set; } + + public void Merge(T args) + { + foreach (var member in typeof(T).GetProperties()) + { + var value = member.GetValue(args); + member.SetValue(this, value); + } + } + + public override string ToString() + { + var sb = new StringBuilder(); + + if (!string.IsNullOrWhiteSpace(Ticket)) + sb.Append("Ticket=\"REDACTED-FOR-PRIVACY\";"); // you're welcome :) + + foreach (var prop in GetType().GetProperties()) + { + if (!prop.CanRead || prop.Name is "Ticket" or "Request") + continue; + + var defaultValue = prop.PropertyType.IsValueType ? Activator.CreateInstance(prop.PropertyType) : null; + var value = prop.GetValue(this); + + if (value != defaultValue) + sb.Append(prop.Name + $"=\"{value}\";"); + } + + return sb.ToString(); + } +} \ No newline at end of file diff --git a/src/Starlight/Launch/Launcher.cs b/src/Starlight/Launch/Launcher.cs new file mode 100644 index 0000000..d6ce7d1 --- /dev/null +++ b/src/Starlight/Launch/Launcher.cs @@ -0,0 +1,196 @@ +using System; +using System.IO; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Forms; +using log4net; +using Starlight.Bootstrap; +using Starlight.Except; +using Starlight.Misc; +using Starlight.PostLaunch; +using static Starlight.Misc.Native; + +namespace Starlight.Launch; + +public class Launcher +{ + // ReSharper disable once PossibleNullReferenceException + internal static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + + /* Singleton */ + + static Mutex _rbxSingleton; + + /// + /// Creates a new systemwide mutex that will allow multiple instances of Roblox from running by making the singleton + /// instance thread yield forever. + /// + public static void CommitSingleton() + { + _rbxSingleton = new Mutex(true, "ROBLOX_singletonMutex"); + } + + /// + /// Closes the systemwide mutex that was created by . + /// + public static void ReleaseSingleton() + { + // Releases on thread exit as well. + _rbxSingleton.Dispose(); + } + + /* Launcher */ + + /// + /// Launches Roblox with the specified arguments. + /// + /// The basic information for launching Roblox. + /// The custom Starlight parameters for launching Roblox. + /// A class that governs the new instance of Roblox. + /// Thrown when anything related to Roblox web services fails. + /// The specfied client doesn't exist. + /// Roblox closed before Starlight could do anything with it. + /// A post-launch task failed. + public static ClientInstance Launch(LaunchParams info, IStarlightLaunchParams extras = null) + { + if (extras != null) + info.Merge(extras); + + if (string.IsNullOrWhiteSpace(info.Hash)) + info.Hash = Bootstrapper.GetLatestHash(); + + Log.Info($"Launch: Preparing to launch Roblox version-{info.Hash}..."); + + // Spoof the trackers if spoofing is enabled + if (info.Spoof) + { + info.TrackerId = Utility.SecureRandomInteger(); + info.LaunchTime = DateTime.Now; + Log.Debug($"Launch: Spoofed BrowserTrackerId to {info.TrackerId} and LaunchTime to {info.LaunchTime}"); + } + + // Get client + Log.Debug($"Launch: Querying {info.Hash}..."); + var client = Bootstrapper.QueryClient(info.Hash); + + // Open Roblox client + Log.Debug("Launch: Native open..."); + if (!OpenRoblox(client.Player, info, out var procInfo)) + { + var ex = new PrematureCloseException(); + Log.Fatal("Launch: Failed to open Roblox.", ex); + throw ex; + } + + ResumeThread(procInfo.hThread); + + // Create an instance + ClientInstance inst; + try + { + inst = new ClientInstance(procInfo.dwProcessId); + } + catch + { + var ex = new PrematureCloseException(); + Log.Fatal("Launch: Failed to initialize Roblox instance.", ex); + throw ex; + } + + // Wait for Roblox's window to open + Log.Debug("Launch: Waiting for Roblox window..."); + + IntPtr hWnd; + var waitStart = DateTime.Now; + while ((hWnd = inst.Proc.MainWindowHandle) == IntPtr.Zero) + { + Thread.Sleep(TimeSpan.FromSeconds(1.0d / 15)); + if (DateTime.Now - waitStart <= TimeSpan.FromSeconds(10)) + continue; + + var ex = new PrematureCloseException(); + Log.Fatal("Launch: Roblox unexpectedly closed.", ex); + throw ex; + } + + Log.Debug("Roblox launched!"); + + /* Runtime */ + + // Set FPS cap + if (info.FpsCap != 0) + { + inst.SetFrameDelay(1.0d / info.FpsCap); + Log.Debug($"Launch: Set FPS cap to {info.FpsCap}."); + } + + // Set resolution of Roblox + if (!string.IsNullOrWhiteSpace(info.Resolution)) + { + var res = Utility.ParseResolution(info.Resolution); + if (res.HasValue) + { + var bounds = Utility.GetWindowBounds(hWnd); + var screenBounds = Screen.PrimaryScreen.WorkingArea with { X = 0, Y = 0 }; + + SetWindowPos( + hWnd, + IntPtr.Zero, + screenBounds.Right / 2 - bounds.Width / 2, // Center X + screenBounds.Bottom / 2 - bounds.Height / 2, // Center Y + res.Value.Item1, + res.Value.Item2, + SWP_NOOWNERZORDER | SWP_NOZORDER); + SetWindowLong(hWnd, GWL_STYLE, WS_POPUPWINDOW); // Remove window styles (title bar, etc.) + ShowWindow(hWnd, SW_SHOW); + + Log.Debug($"Launch: Set resolution to {info.Resolution}."); + } + else + { + Log.Error("Launch: Skipped setting resolution because parse failed."); + } + } + + // Enter headless mode + if (info.Headless) + { + SendMessage(hWnd, WM_SYSCOMMAND, SC_MINIMIZE, + IntPtr.Zero); // Just learned that minimize = no render :thumbsup: + ShowWindow(hWnd, SW_HIDE); + Log.Debug("Launch: Roblox window hidden. Client is now headless."); + } + + // Attach + if (info.AttachMethod != AttachMethod.None) + inst.Attach(info.AttachMethod); + + Log.Debug("Launch: Finished post-launch."); + Log.Info("Launch: Roblox launched."); + + return inst; + } + + public static async Task LaunchAsync(LaunchParams info, IStarlightLaunchParams extras = null) + { + return await Task.Run(() => Launch(info, extras)); + } + + internal static bool OpenRoblox(string robloxPath, LaunchParams info, out PROCESS_INFORMATION procInfo) + { + STARTUPINFO startInfo = new(); + return CreateProcess( + Path.GetFullPath(robloxPath), + $"--play -a https://auth.roblox.com/v1/authentication-ticket/redeem -t {info.Ticket} -j {info.Request.Serialize()} -b {info.TrackerId} " + // Updated to another endpoint. + $"--launchtime={info.LaunchTime.ToUnixTimeMilliseconds()} --rloc {info.RobloxLocale.Name} --gloc {info.GameLocale.Name}", + 0, + 0, + false, + ProcessCreationFlags.CREATE_SUSPENDED, + 0, + null, + ref startInfo, + out procInfo); + } +} \ No newline at end of file diff --git a/src/Starlight/Misc/Extensions.cs b/src/Starlight/Misc/Extensions.cs index 5cc82f8..3c1dbe1 100644 --- a/src/Starlight/Misc/Extensions.cs +++ b/src/Starlight/Misc/Extensions.cs @@ -5,94 +5,102 @@ using System.Linq; using System.Net.Http; -namespace Starlight.Misc +namespace Starlight.Misc; + +internal static class Extensions { - internal static class Extensions + /* IEnumerable */ + + internal static void ForEach(this IEnumerable source, Action action) { - /* IEnumerable */ + foreach (var element in source) + action(element); + } - internal static void ForEach(this IEnumerable source, Action action) - { - foreach (var element in source) - action(element); - } + /* string */ + + internal static string[] Split(this string str, string delim) + { + return str.Split(new[] { delim }, StringSplitOptions.None); + } + + internal static string[] Split(this string str, params string[] delim) + { + return str.Split(delim, StringSplitOptions.None); + } + + internal static string[] Split(this string str, StringSplitOptions opt, params string[] delim) + { + return str.Split(delim, StringSplitOptions.None); + } + + internal static string[] Split(this string str, string delim, StringSplitOptions opt) + { + return str.Split(new[] { delim }, opt); + } - /* string */ + /* HttpClient */ - internal static string[] Split(this string str, string delim) => - str.Split(new[] { delim }, StringSplitOptions.None); - - internal static string[] Split(this string str, params string[] delim) => - str.Split(delim, StringSplitOptions.None); + internal static string DownloadString(this HttpClient client, string url) + { + using var stm = client.GetStreamAsync(url).Result; + using StreamReader rstm = new(stm); + return rstm.ReadToEnd(); + } - internal static string[] Split(this string str, StringSplitOptions opt, params string[] delim) => - str.Split(delim, StringSplitOptions.None); + internal static void DownloadFile(this HttpClient client, string url, string filePath) + { + using var stm = client.GetStreamAsync(url).Result; + using var fstm = File.Create(filePath); + stm.CopyTo(fstm); + } - internal static string[] Split(this string str, string delim, StringSplitOptions opt) => - str.Split(new[] { delim }, opt); + /* ZipArchive (because whoever wrote it doesn't know what the hell overwriting means) */ - /* HttpClient */ - - internal static string DownloadString(this HttpClient client, string url) + // Roblox does something funny with the zip files so I have to redo their code...also I need overwriting. + // Pasted from StackOverflow, still had to rewrite it because the code itself just didn't wanna work. :/ + internal static void ExtractToDirectory(this ZipArchive archive, string destDir, bool overwrite) + { + if (!overwrite) { - using var stm = client.GetStreamAsync(url).Result; - using StreamReader rstm = new(stm); - return rstm.ReadToEnd(); + archive.ExtractToDirectory(destDir); + return; } - internal static void DownloadFile(this HttpClient client, string url, string filePath) + destDir = Directory.CreateDirectory(destDir).FullName; + foreach (var file in archive.Entries) { - using var stm = client.GetStreamAsync(url).Result; - using var fstm = File.Create(filePath); - stm.CopyTo(fstm); - } + if (file.FullName.StartsWith("\\")) continue; // Roblox raping the zip files. + var completeFileName = Path.GetFullPath(Path.Combine(destDir, file.FullName)); - /* ZipArchive (because whoever wrote it doesn't know what the hell overwriting means) */ + if (!completeFileName.StartsWith(destDir, StringComparison.OrdinalIgnoreCase)) + throw new IOException( + "Trying to extract file outside of destination directory. See this link for more info: https://snyk.io/research/zip-slip-vulnerability"); - // Roblox does something funny with the zip files so I have to redo their code...also I need overwriting. - // Pasted from StackOverflow, still had to rewrite it because the code itself just didn't wanna work. :/ - internal static void ExtractToDirectory(this ZipArchive archive, string destDir, bool overwrite) - { - if (!overwrite) + // ReSharper disable AssignNullToNotNullAttribute + var dirName = Path.GetDirectoryName(completeFileName); + if (!Directory.Exists(dirName)) + Directory.CreateDirectory(dirName); + + if (file.Name == "") { - archive.ExtractToDirectory(destDir); - return; + Directory.CreateDirectory(dirName); + continue; } - - destDir = Directory.CreateDirectory(destDir).FullName; - foreach (var file in archive.Entries) - { - if (file.FullName.StartsWith("\\")) continue; // Roblox raping the zip files. - var completeFileName = Path.GetFullPath(Path.Combine(destDir, file.FullName)); + // ReSharper enable AssignNullToNotNullAttribute - if (!completeFileName.StartsWith(destDir, StringComparison.OrdinalIgnoreCase)) - throw new IOException("Trying to extract file outside of destination directory. See this link for more info: https://snyk.io/research/zip-slip-vulnerability"); - - // ReSharper disable AssignNullToNotNullAttribute - var dirName = Path.GetDirectoryName(completeFileName); - if (!Directory.Exists(dirName)) - Directory.CreateDirectory(dirName); + file.ExtractToFile(completeFileName, true); + } + } - if (file.Name == "") - { - Directory.CreateDirectory(dirName); - continue; - } - // ReSharper enable AssignNullToNotNullAttribute - file.ExtractToFile(completeFileName, true); - } - } - + internal static void ExtractSelectedToDirectory(this ZipArchive file, string destDir, string selector) + { + var entries = file.Entries.Where(entry => selector == entry.FullName); + var archiveEntries = entries as ZipArchiveEntry[] ?? entries.ToArray(); + if (!archiveEntries.Any()) + throw new IOException($"\"{selector}\" does not exist in the zip file."); - internal static void ExtractSelectedToDirectory(this ZipArchive file, string destDir, string selector) - { - var entries = file.Entries.Where(entry => selector == entry.FullName); - var archiveEntries = entries as ZipArchiveEntry[] ?? entries.ToArray(); - if (!archiveEntries.Any()) - throw new IOException($"\"{selector}\" does not exist in the zip file."); - - archiveEntries.ForEach(entry => entry.ExtractToFile(Path.Combine(destDir, entry.FullName))); - } + archiveEntries.ForEach(entry => entry.ExtractToFile(Path.Combine(destDir, entry.FullName))); } -} +} \ No newline at end of file diff --git a/src/Starlight/Misc/Logger.cs b/src/Starlight/Misc/Logger.cs index 064dd18..940fd1b 100644 --- a/src/Starlight/Misc/Logger.cs +++ b/src/Starlight/Misc/Logger.cs @@ -1,42 +1,42 @@ -using log4net; +using System; +using log4net; using log4net.Appender; using log4net.Core; using log4net.Layout; using log4net.Repository.Hierarchy; -using System; -namespace Starlight.Misc +namespace Starlight.Misc; + +public class Logger { - public class Logger - { - public static string LogFile { get; protected set; } + public static string LogFile { get; protected set; } - public static void Init(bool verbose) - { - var hierarchy = (Hierarchy)LogManager.GetRepository(); + public static void Init(bool verbose) + { + var hierarchy = (Hierarchy)LogManager.GetRepository(); - var patternLayout = new PatternLayout { ConversionPattern = "%date [%thread] %-5level %logger - %message%newline" }; - patternLayout.ActivateOptions(); + var patternLayout = new PatternLayout + { ConversionPattern = "%date [%thread] %-5level %logger - %message%newline" }; + patternLayout.ActivateOptions(); - var roller = new RollingFileAppender - { - AppendToFile = false, - File = LogFile = $"Logs/Starlight-{DateTime.Now:MM-dd-yyyy-HH-mm-ss}.txt", - Layout = patternLayout, - MaxSizeRollBackups = 5, - MaximumFileSize = "10MB", - RollingStyle = RollingFileAppender.RollingMode.Size, - StaticLogFileName = true - }; - roller.ActivateOptions(); - hierarchy.Root.AddAppender(roller); + var roller = new RollingFileAppender + { + AppendToFile = false, + File = LogFile = $"Logs/Starlight-{DateTime.Now:MM-dd-yyyy-HH-mm-ss}.txt", + Layout = patternLayout, + MaxSizeRollBackups = 5, + MaximumFileSize = "10MB", + RollingStyle = RollingFileAppender.RollingMode.Size, + StaticLogFileName = true + }; + roller.ActivateOptions(); + hierarchy.Root.AddAppender(roller); - var memory = new MemoryAppender(); - memory.ActivateOptions(); - hierarchy.Root.AddAppender(memory); + var memory = new MemoryAppender(); + memory.ActivateOptions(); + hierarchy.Root.AddAppender(memory); - hierarchy.Root.Level = verbose ? Level.Debug : Level.Info; - hierarchy.Configured = true; - } + hierarchy.Root.Level = verbose ? Level.Debug : Level.Info; + hierarchy.Configured = true; } } \ No newline at end of file diff --git a/src/Starlight/Misc/Native.cs b/src/Starlight/Misc/Native.cs index c439db9..ac95d48 100644 --- a/src/Starlight/Misc/Native.cs +++ b/src/Starlight/Misc/Native.cs @@ -1,201 +1,213 @@ using System; using System.Runtime.InteropServices; -namespace Starlight.Misc +namespace Starlight.Misc; + +internal class Native { - public class Native + [Flags] + public enum ContextFlags : uint + { + I386 = 0x10000, + Control = I386 | 0x01, + Integer = I386 | 0x02, + Segments = I386 | 0x04, + FloatingPoint = I386 | 0x08, + DebugRegisters = I386 | 0x10, + ExtendedRegisers = I386 | 0x20, + Full = Control | Integer | Segments, + All = Control | Integer | Segments | FloatingPoint | DebugRegisters | ExtendedRegisers + } + + [Flags] + public enum ProcessCreationFlags : uint // got lazy and didnt rename anything here + { + ZERO_FLAG = 0x00000000, + CREATE_BREAKAWAY_FROM_JOB = 0x01000000, + CREATE_DEFAULT_ERROR_MODE = 0x04000000, + CREATE_NEW_CONSOLE = 0x00000010, + CREATE_NEW_PROCESS_GROUP = 0x00000200, + CREATE_NO_WINDOW = 0x08000000, + CREATE_PROTECTED_PROCESS = 0x00040000, + CREATE_PRESERVE_CODE_AUTHZ_LEVEL = 0x02000000, + CREATE_SEPARATE_WOW_VDM = 0x00001000, + CREATE_SHARED_WOW_VDM = 0x00001000, + CREATE_SUSPENDED = 0x00000004, + CREATE_UNICODE_ENVIRONMENT = 0x00000400, + DEBUG_ONLY_THIS_PROCESS = 0x00000002, + DEBUG_PROCESS = 0x00000001, + DETACHED_PROCESS = 0x00000008, + EXTENDED_STARTUPINFO_PRESENT = 0x00080000, + INHERIT_PARENT_AFFINITY = 0x00010000 + } + + [Flags] + public enum ThreadAccess : uint + { + Terminate = 0x0001, + SuspendResume = 0x0002, + GetContext = 0x0008, + SetContext = 0x0010, + SetInfo = 0x0020, + QueryInfo = 0x0040, + SetToken = 0x0080, + Impersonate = 0x0100, + DirectImpersonate = 0x0200 + } + + public const int WM_SYSCOMMAND = 0x112; + public const int SC_MINIMIZE = 0xF020; + + public const uint SWP_NOSIZE = 0x0001; + public const uint SWP_NOMOVE = 0x0002; + public const uint SWP_NOZORDER = 0x0004; + public const uint SWP_NOREDRAW = 0x0008; + public const uint SWP_NOACTIVATE = 0x0010; + public const uint SWP_DRAWFRAME = 0x0020; + public const uint SWP_FRAMECHANGED = 0x0020; + public const uint SWP_SHOWWINDOW = 0x0040; + public const uint SWP_HIDEWINDOW = 0x0080; + public const uint SWP_NOCOPYBITS = 0x0100; + public const uint SWP_NOOWNERZORDER = 0x0200; + public const uint SWP_NOREPOSITION = 0x0200; + public const uint SWP_NOSENDCHANGING = 0x0400; + public const uint SWP_DEFERERASE = 0x2000; + public const uint SWP_ASYNCWINDOWPOS = 0x4000; + + public const uint TOPMOST_FLAGS = SWP_NOMOVE | SWP_NOSIZE; + public const uint NOTOPMOST_FLAGS = SWP_SHOWWINDOW; + + public const int SW_SHOW = 5; + public const int SW_HIDE = 0; + public const uint WS_POPUPWINDOW = 0x80880000; + + public const int GWL_STYLE = -16; + + public static readonly IntPtr HWND_TOPMOST = new(-1); + public static readonly IntPtr HWND_NOTOPMOST = new(-2); + + [DllImport("user32.dll", CharSet = CharSet.Auto)] + public static extern IntPtr SendMessage(IntPtr hWnd, int msg, int wParam, IntPtr lParam); + + [DllImport("user32.dll", SetLastError = true)] + public static extern bool GetWindowRect(IntPtr hWnd, out NativeRect lpRect); + + [DllImport("user32.dll", SetLastError = true)] + public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); + + [DllImport("user32.dll", SetLastError = true)] + public static extern IntPtr SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int Y, int cx, int cy, + uint wFlags); + + [DllImport("user32.dll")] + public static extern int SetWindowLong(IntPtr hWnd, int nIndex, uint dwNewLong); + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] + public static extern bool CreateProcess(string lpApplicationName, string lpCommandLine, uint lpProcessAttributes, + uint lpThreadAttributes, bool bInheritHandles, ProcessCreationFlags dwCreationFlags, uint lpEnvironment, + string lpCurrentDirectory, [In] ref STARTUPINFO lpStartupInfo, + [Out] out PROCESS_INFORMATION lpProcessInformation); + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Ansi)] + public static extern bool CloseHandle(int hThread); + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Ansi)] + public static extern int SuspendThread(int hThread); + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Ansi)] + public static extern int ResumeThread(int hThread); + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Ansi)] + public static extern bool GetThreadContext(int hThread, ref ThreadContext lpContext); + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Ansi)] + public static extern bool SetThreadContext(int hThread, ref ThreadContext lpContext); + + [StructLayout(LayoutKind.Sequential)] + public struct NativeRect + { + public int Left; + public int Top; + public int Right; + public int Bottom; + } + + public struct STARTUPINFO + { + public uint cb; + public string lpReserved; + public string lpDesktop; + public string lpTitle; + public uint dwX; + public uint dwY; + public uint dwXSize; + public uint dwYSize; + public uint dwXCountChars; + public uint dwYCountChars; + public uint dwFillAttribute; + public uint dwFlags; + public short wShowWindow; + public short cbReserved2; + public IntPtr lpReserved2; + public IntPtr hStdInput; + public IntPtr hStdOutput; + public IntPtr hStdError; + } + + public struct PROCESS_INFORMATION + { + public int hProcess; + public int hThread; + public int dwProcessId; + public int dwThreadId; + } + + [StructLayout(LayoutKind.Sequential)] + public struct XmmSave + { + public int ControlWord; + public int StatusWord; + public int TagWord; + public int ErrorOffset; + public int ErrorSelector; + public int DataOffset; + public int DataSelector; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 80)] + public byte[] RegisterArea; + + public int Cr0NpxState; + } + + [StructLayout(LayoutKind.Sequential)] + public struct ThreadContext { - [DllImport("user32.dll", SetLastError = true)] - public static extern bool GetWindowRect(IntPtr hWnd, out NativeRect lpRect); - - [StructLayout(LayoutKind.Sequential)] - public struct NativeRect - { - public int Left; - public int Top; - public int Right; - public int Bottom; - } - - [DllImport("user32.dll", SetLastError = true)] - public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); - - [DllImport("user32.dll", SetLastError = true)] - public static extern IntPtr SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int Y, int cx, int cy, uint wFlags); - - public static readonly IntPtr HWND_TOPMOST = new(-1); - public static readonly IntPtr HWND_NOTOPMOST = new(-2); - - public const uint SWP_NOSIZE = 0x0001; - public const uint SWP_NOMOVE = 0x0002; - public const uint SWP_NOZORDER = 0x0004; - public const uint SWP_NOREDRAW = 0x0008; - public const uint SWP_NOACTIVATE = 0x0010; - public const uint SWP_DRAWFRAME = 0x0020; - public const uint SWP_FRAMECHANGED = 0x0020; - public const uint SWP_SHOWWINDOW = 0x0040; - public const uint SWP_HIDEWINDOW = 0x0080; - public const uint SWP_NOCOPYBITS = 0x0100; - public const uint SWP_NOOWNERZORDER = 0x0200; - public const uint SWP_NOREPOSITION = 0x0200; - public const uint SWP_NOSENDCHANGING = 0x0400; - public const uint SWP_DEFERERASE = 0x2000; - public const uint SWP_ASYNCWINDOWPOS = 0x4000; - - public const uint TOPMOST_FLAGS = SWP_NOMOVE | SWP_NOSIZE; - public const uint NOTOPMOST_FLAGS = SWP_SHOWWINDOW; - - public const int SW_SHOW = 5; - public const int SW_HIDE = 0; - public const uint WS_POPUPWINDOW = 0x80880000; - - [DllImport("user32.dll")] - public static extern int SetWindowLong(IntPtr hWnd, int nIndex, uint dwNewLong); - - public const int GWL_STYLE = -16; - - [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] - public static extern bool CreateProcess(string lpApplicationName, string lpCommandLine, uint lpProcessAttributes, uint lpThreadAttributes, bool bInheritHandles, ProcessCreationFlags dwCreationFlags, uint lpEnvironment, string lpCurrentDirectory, [In] ref STARTUPINFO lpStartupInfo, [Out] out PROCESS_INFORMATION lpProcessInformation); - - [Flags] - public enum ProcessCreationFlags : uint // got lazy and didnt rename anything here - { - ZERO_FLAG = 0x00000000, - CREATE_BREAKAWAY_FROM_JOB = 0x01000000, - CREATE_DEFAULT_ERROR_MODE = 0x04000000, - CREATE_NEW_CONSOLE = 0x00000010, - CREATE_NEW_PROCESS_GROUP = 0x00000200, - CREATE_NO_WINDOW = 0x08000000, - CREATE_PROTECTED_PROCESS = 0x00040000, - CREATE_PRESERVE_CODE_AUTHZ_LEVEL = 0x02000000, - CREATE_SEPARATE_WOW_VDM = 0x00001000, - CREATE_SHARED_WOW_VDM = 0x00001000, - CREATE_SUSPENDED = 0x00000004, - CREATE_UNICODE_ENVIRONMENT = 0x00000400, - DEBUG_ONLY_THIS_PROCESS = 0x00000002, - DEBUG_PROCESS = 0x00000001, - DETACHED_PROCESS = 0x00000008, - EXTENDED_STARTUPINFO_PRESENT = 0x00080000, - INHERIT_PARENT_AFFINITY = 0x00010000 - } - - public struct STARTUPINFO - { - public uint cb; - public string lpReserved; - public string lpDesktop; - public string lpTitle; - public uint dwX; - public uint dwY; - public uint dwXSize; - public uint dwYSize; - public uint dwXCountChars; - public uint dwYCountChars; - public uint dwFillAttribute; - public uint dwFlags; - public short wShowWindow; - public short cbReserved2; - public IntPtr lpReserved2; - public IntPtr hStdInput; - public IntPtr hStdOutput; - public IntPtr hStdError; - } - - public struct PROCESS_INFORMATION - { - public int hProcess; - public int hThread; - public int dwProcessId; - public int dwThreadId; - } - - [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Ansi)] - public static extern bool CloseHandle(int hThread); - - [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Ansi)] - public static extern int SuspendThread(int hThread); - - [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Ansi)] - public static extern int ResumeThread(int hThread); - - [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Ansi)] - public static extern bool GetThreadContext(int hThread, ref ThreadContext lpContext); - - [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Ansi)] - public static extern bool SetThreadContext(int hThread, ref ThreadContext lpContext); - - [Flags] - public enum ThreadAccess : uint - { - Terminate = 0x0001, - SuspendResume = 0x0002, - GetContext = 0x0008, - SetContext = 0x0010, - SetInfo = 0x0020, - QueryInfo = 0x0040, - SetToken = 0x0080, - Impersonate = 0x0100, - DirectImpersonate = 0x0200, - } - - [Flags] - public enum ContextFlags : uint - { - I386 = 0x10000, - Control = I386 | 0x01, - Integer = I386 | 0x02, - Segments = I386 | 0x04, - FloatingPoint = I386 | 0x08, - DebugRegisters = I386 | 0x10, - ExtendedRegisers = I386 | 0x20, - Full = Control | Integer | Segments, - All = Control | Integer | Segments | FloatingPoint | DebugRegisters | ExtendedRegisers, - } - - [StructLayout(LayoutKind.Sequential)] - public struct XmmSave - { - public int ControlWord; - public int StatusWord; - public int TagWord; - public int ErrorOffset; - public int ErrorSelector; - public int DataOffset; - public int DataSelector; - [MarshalAs(UnmanagedType.ByValArray, SizeConst = 80)] - public byte[] RegisterArea; - public int Cr0NpxState; - } - - [StructLayout(LayoutKind.Sequential)] - public struct ThreadContext - { - public ContextFlags Flags; - public uint Dr0; - public uint Dr1; - public uint Dr2; - public uint Dr3; - public uint Dr6; - public uint Dr7; - public XmmSave Xmm; - public uint SegGs; - public uint SegFs; - public uint SegEs; - public uint SegDs; - public uint Edi; - public uint Esi; - public uint Ebx; - public uint Edx; - public uint Ecx; - public uint Eax; - public uint Ebp; - public uint Eip; - public uint SegCs; - public uint EFlags; - public uint Esp; - public uint SegSs; - [MarshalAs(UnmanagedType.ByValArray, SizeConst = 512)] - public byte[] ExtendedRegisters; - } + public ContextFlags Flags; + public uint Dr0; + public uint Dr1; + public uint Dr2; + public uint Dr3; + public uint Dr6; + public uint Dr7; + public XmmSave Xmm; + public uint SegGs; + public uint SegFs; + public uint SegEs; + public uint SegDs; + public uint Edi; + public uint Esi; + public uint Ebx; + public uint Edx; + public uint Ecx; + public uint Eax; + public uint Ebp; + public uint Eip; + public uint SegCs; + public uint EFlags; + public uint Esp; + public uint SegSs; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 512)] + public byte[] ExtendedRegisters; } } \ No newline at end of file diff --git a/src/Starlight/Misc/Shared.cs b/src/Starlight/Misc/Shared.cs index 2102f7c..cb3f729 100644 --- a/src/Starlight/Misc/Shared.cs +++ b/src/Starlight/Misc/Shared.cs @@ -1,9 +1,8 @@ using System.Net.Http; -namespace Starlight.Misc +namespace Starlight.Misc; + +internal static class Shared { - internal static class Shared - { - internal static readonly HttpClient Web = new(); - } + internal static readonly HttpClient Web = new(); } \ No newline at end of file diff --git a/src/Starlight/Misc/Utility.cs b/src/Starlight/Misc/Utility.cs index 4f2307c..3f19bc0 100644 --- a/src/Starlight/Misc/Utility.cs +++ b/src/Starlight/Misc/Utility.cs @@ -1,128 +1,158 @@ -using IWshRuntimeLibrary; -using log4net; -using System; +using System; using System.Collections.Generic; using System.Drawing; using System.Globalization; using System.IO; using System.Linq; +using System.Reflection; using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; +using IWshRuntimeLibrary; +using log4net; using static Starlight.Misc.Native; -namespace Starlight.Misc +namespace Starlight.Misc; + +internal class Utility { - internal class Utility + // ReSharper disable once PossibleNullReferenceException + internal static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + + public static string GetTempDir() + { + var dir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + Directory.CreateDirectory(dir); + return dir; + } + + public static void CreateShortcut(string filePath, string target, string workingDir) { - // ReSharper disable once PossibleNullReferenceException - internal static readonly ILog Log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); + WshShell shell = new(); // This is a nasty library; I wish COM didn't exist. + var shortcut = (IWshShortcut)shell.CreateShortcut(filePath); - public static string GetTempDir() + shortcut.TargetPath = target; + shortcut.WorkingDirectory = workingDir; + shortcut.Save(); + } + + public static int SecureRandomInteger() + { + using RNGCryptoServiceProvider rng = new(); + var seed = new byte[4]; + rng.GetBytes(seed); + return new Random(BitConverter.ToInt32(seed, 0)).Next(); + } + + public static bool TryGetCultureInfo(string name, out CultureInfo ci) + { + try { - var dir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); - Directory.CreateDirectory(dir); - return dir; + ci = new CultureInfo(name, false); + return true; } - - public static void CreateShortcut(string filePath, string target, string workingDir) + catch (CultureNotFoundException) { - WshShell shell = new(); // This is a nasty library; I wish COM didn't exist. - var shortcut = (IWshShortcut)shell.CreateShortcut(filePath); - - shortcut.TargetPath = target; - shortcut.WorkingDirectory = workingDir; - shortcut.Save(); + ci = null; + return false; } + } + + public static (int, int)? ParseResolution(string res) + { + var parts = res.Split('x'); + if (parts.Length != 2) + return null; + + var b = true; + b &= int.TryParse(parts[0], out var p1); + b &= int.TryParse(parts[1], out var p2); + + return b ? (p1, p2) : null; + } + + public static Rectangle GetWindowBounds(IntPtr hWnd) + { + if (!GetWindowRect(hWnd, out var nRect)) + return Rectangle.Empty; - public static int SecureRandomInteger() + return new Rectangle { - using RNGCryptoServiceProvider rng = new(); - var seed = new byte[4]; - rng.GetBytes(seed); - return new Random(BitConverter.ToInt32(seed, 0)).Next(); - } + X = nRect.Left, + Y = nRect.Top, + Width = nRect.Right - nRect.Left, + Height = nRect.Bottom - nRect.Top + }; + } - public static bool TryGetCultureInfo(string name, out CultureInfo ci) + public static void DisperseActions(IReadOnlyList actions, int maxConcurrency) + { + var curConcurrency = 0; + var threadFinishedEvent = new AutoResetEvent(false); + + foreach (var action in actions) { - try + var thread = new Thread(() => { - ci = new CultureInfo(name, false); - return true; - } - catch (CultureNotFoundException) + action(); + threadFinishedEvent.Set(); + }); + + thread.Start(); + curConcurrency++; + + while (curConcurrency >= maxConcurrency) { - ci = null; - return false; + Log.Debug("DisperseActions: Waiting for available thread slot..."); + threadFinishedEvent.WaitOne(); + curConcurrency--; } } - public static (int, int)? ParseResolution(string res) + while (curConcurrency > 0) { - var parts = res.Split('x'); - if (parts.Length != 2) - return null; - - var b = true; - b &= int.TryParse(parts[0], out var p1); - b &= int.TryParse(parts[1], out var p2); - - return b ? (p1, p2) : null; + Log.Debug($"DisperseActions: {curConcurrency} threads left"); + threadFinishedEvent.WaitOne(); + curConcurrency--; } + } - public static Rectangle GetWindowBounds(IntPtr hWnd) - { - if (!GetWindowRect(hWnd, out var nRect)) - return Rectangle.Empty; + public static async Task DisperseActionsAsync(IReadOnlyList actions, int maxConcurrency) + { + await Task.Run(() => DisperseActions(actions, maxConcurrency)); + } - return new Rectangle - { - X = nRect.Left, - Y = nRect.Top, - Width = nRect.Right - nRect.Left, - Height = nRect.Bottom - nRect.Top - }; + public static void DisperseActions(IReadOnlyList list, Action action, int maxConcurrency) + { + DisperseActions(list.Select(x => new Action(() => action(x))).ToList(), maxConcurrency); + } + + public static async Task DisperseActionsAsync(IReadOnlyList list, Action action, int maxConcurrency) + { + await Task.Run(() => DisperseActions(list, action, maxConcurrency)); + } + + public static bool CanShare(string path, FileShare flags) + { + try + { + using var sr = new FileStream(path, FileMode.Open, FileAccess.Read, flags); + return sr.Length > 0; + } + catch (Exception) + { + return false; } + } - public static void DisperseActions(IReadOnlyList actions, int maxConcurrency) + public static void WaitShare(string path, FileShare flags, CancellationToken ct = default) + { + while (!CanShare(path, flags)) { - var curConcurrency = 0; - var threadFinishedEvent = new AutoResetEvent(false); - - foreach (var action in actions) - { - var thread = new Thread(() => - { - action(); - threadFinishedEvent.Set(); - }); - - thread.Start(); - curConcurrency++; - - while (curConcurrency >= maxConcurrency) - { - Log.Debug("DisperseActions: Waiting for available thread slot..."); - threadFinishedEvent.WaitOne(); - curConcurrency--; - } - } - - while (curConcurrency > 0) - { - Log.Debug($"DisperseActions: {curConcurrency} threads left"); - threadFinishedEvent.WaitOne(); - curConcurrency--; - } + if (ct.IsCancellationRequested) + return; + + Thread.Sleep(100); } - - public static async Task DisperseActionsAsync(IReadOnlyList actions, int maxConcurrency) => - await Task.Run(() => DisperseActions(actions, maxConcurrency)); - - public static void DisperseActions(IReadOnlyList list, Action action, int maxConcurrency) => - DisperseActions(list.Select(x => new Action(() => action(x))).ToList(), maxConcurrency); - - public static async Task DisperseActionsAsync(IReadOnlyList list, Action action, int maxConcurrency) => - await Task.Run(() => DisperseActions(list, action, maxConcurrency)); } -} +} \ No newline at end of file diff --git a/src/Starlight/PostLaunch/AttachMethod.cs b/src/Starlight/PostLaunch/AttachMethod.cs new file mode 100644 index 0000000..0a03225 --- /dev/null +++ b/src/Starlight/PostLaunch/AttachMethod.cs @@ -0,0 +1,7 @@ +namespace Starlight.PostLaunch; + +public enum AttachMethod +{ + None, + Synapse +} \ No newline at end of file diff --git a/src/Starlight/PostLaunch/ClientInstance.cs b/src/Starlight/PostLaunch/ClientInstance.cs new file mode 100644 index 0000000..ec18cfa --- /dev/null +++ b/src/Starlight/PostLaunch/ClientInstance.cs @@ -0,0 +1,229 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using HackerFramework; +using log4net; +using Starlight.Except; +using sxlib; +using sxlib.Specialized; + +namespace Starlight.PostLaunch; + +public class ClientInstance +{ + // ReSharper disable once PossibleNullReferenceException + internal static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + + static SxLibOffscreen _libSx; + + public readonly Process Proc; + + public readonly Target Rbx; + + uint _frameDelayOff; + + TaskScheduler _taskScheduler; + + long _userId; + + public ClientInstance(int procId) + { + Proc = Process.GetProcessById(procId); + if (Proc is null || Proc.HasExited) + { + var ex = new PrematureCloseException(); + Log.Fatal("Roblox prematurely exited.", ex); + throw ex; + } + + Rbx = new Target(Proc); + } + + public long GetUserId() + { + if (_userId != 0) + return _userId; + + var results = Rbx.FindPattern(RobloxData.UserIdSignature); + if (results.Count is 0 or > 1) + { + var ex = new PostLaunchException("Failed to find UserId."); + Log.Fatal("UserId signature scan failed. Signature update required.", ex); + throw ex; + } + + var userIdAddr = Rbx.ReadPointer(results[0] + RobloxData.UserIdOffset); + long userId; + while ((userId = Rbx.ReadLong(userIdAddr)) == 0 || userId == -1) + Thread.Sleep(100); + + _userId = userId; + return userId; + } + + public async Task GetUserIdAsync() + { + return await Task.Run(GetUserId); + } + + public TaskScheduler GetTaskScheduler() + { + if (_taskScheduler is not null) + return _taskScheduler; + + var results = Rbx.FindPattern(RobloxData.TaskSchedulerSignature); + if (results.Count is 0 or > 1) + { + var ex = new PostLaunchException("Failed to find TaskScheduler."); + Log.Fatal("TaskScheduler signature scan failed. Signature update required.", ex); + throw ex; + } + + var sched = new TaskScheduler { Instance = this }; + var singletonPtr = Rbx.ReadPointer(results[0] + RobloxData.TaskSchedulerOffset); + while ((sched.BaseAddress = Rbx.ReadPointer(singletonPtr)) == 0) + Thread.Sleep(100); + + _taskScheduler = sched; + return sched; + } + + public async Task GetTaskSchedulerAsync() + { + return await Task.Run(GetTaskScheduler); + } + + public void SetFrameDelay(double delay) + { + var sched = GetTaskScheduler(); + if (_frameDelayOff == 0) + { + const double defaultDelay = 1.0d / 60.0d; // 60hz + for (var off = sched.BaseAddress; off < sched.BaseAddress + 0x1000 - sizeof(double); off += 4) + { + var diff = Rbx.ReadDouble(off) - defaultDelay; + if (!(Math.Abs(diff) < 0.001)) + continue; + + _frameDelayOff = off - sched.BaseAddress; + break; + } + + if (_frameDelayOff == 0) + { + var ex = new PostLaunchException("Failed to find FrameDelay."); + Log.Fatal( + "FrameDelay offset scan failed. Possibly an invalid TaskScheduler object. Signature update MAY be required.", + ex); + throw ex; + } + } + + sched.WriteDouble(_frameDelayOff, delay); + } + + public async Task SetFrameDelayAsync(double delay) + { + await Task.Run(() => SetFrameDelay(delay)); + } + + static void SynInit() + { + if (_libSx is not null) + return; + + var sxPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ".."); + PostLaunchException ex; + + if (!File.Exists(Path.Combine(sxPath, "Synapse Launcher.exe"))) + { + ex = new PostLaunchException("Could not attach; Synapse X directory not found."); + Log.Fatal("Could not attach; Synapse X directory not found.", ex); + throw ex; + } + + _libSx = SxLib.InitializeOffscreen(sxPath); + + using var wh = new ManualResetEvent(false); + _libSx.LoadEvent += (e, _) => + { + switch (e) + { + case SxLibBase.SynLoadEvents.READY: + wh.Set(); + break; + case SxLibBase.SynLoadEvents.UNKNOWN: + throw new PostLaunchException("Unknown Synapse error."); + case SxLibBase.SynLoadEvents.NOT_LOGGED_IN: + throw new PostLaunchException("Not logged into Synapse."); + case SxLibBase.SynLoadEvents.NOT_UPDATED: + throw new PostLaunchException("Synapse has not updated yet."); + case SxLibBase.SynLoadEvents.FAILED_TO_VERIFY: + throw new PostLaunchException("Couldn't verify Synapse ownership."); + case SxLibBase.SynLoadEvents.FAILED_TO_DOWNLOAD: + throw new PostLaunchException("Failed to download Synapse DLLs."); + case SxLibBase.SynLoadEvents.UNAUTHORIZED_HWID: + throw new PostLaunchException("You are not authorized to use Synapse."); + case SxLibBase.SynLoadEvents.ALREADY_EXISTING_WL: + throw new PostLaunchException("what"); + case SxLibBase.SynLoadEvents.NOT_ENOUGH_TIME: + throw new PostLaunchException("Synapse initialization timed out."); + case SxLibBase.SynLoadEvents.CHECKING_WL: + break; + case SxLibBase.SynLoadEvents.CHANGING_WL: + break; + case SxLibBase.SynLoadEvents.DOWNLOADING_DATA: + break; + case SxLibBase.SynLoadEvents.CHECKING_DATA: + break; + case SxLibBase.SynLoadEvents.DOWNLOADING_DLLS: + break; + default: + throw new ArgumentOutOfRangeException(nameof(e), e, null); + } + }; + + _libSx.Load(); + if (wh.WaitOne(TimeSpan.FromSeconds(60))) + return; + + ex = new PostLaunchException("Synapse X init timed out."); + Log.Fatal("Synapse X init timed out.", ex); + throw ex; + } + + static void SynAttach() + { + SynInit(); + if (_libSx.Attach()) + return; + + var ex = new PostLaunchException("Synapse X failed to attach."); + Log.Fatal("Synapse X failed to attach.", ex); + throw ex; + } + + public void Attach(AttachMethod method) + { + switch (method) + { + case AttachMethod.Synapse: + SynAttach(); + break; + case AttachMethod.None: + default: + throw new NotImplementedException(); + } + } + + public async Task AttachAsync(AttachMethod method) => + await Task.Run(() => Attach(method)); + + ~ClientInstance() + { + Rbx.Dispose(); + } +} \ No newline at end of file diff --git a/src/Starlight/PostLaunch/RobloxData.cs b/src/Starlight/PostLaunch/RobloxData.cs new file mode 100644 index 0000000..e4618d7 --- /dev/null +++ b/src/Starlight/PostLaunch/RobloxData.cs @@ -0,0 +1,26 @@ +using HackerFramework; + +namespace Starlight.PostLaunch; +// The signatures and offsets stored here may need to be +// updated if Roblox chooses to change their code, which +// is highly unlikely to happen. + +internal class RobloxData +{ + public const uint + TaskSchedulerOffset = 49; // mov eax, dword_xxxxxxxx, instruction should appear twice for same func + + public const uint UserIdOffset = 2; // skips to rel32 of push instruction + + // TaskScheduler::singleton + // string: "Load ClientAppSettings", last four calls + // todo: Maybe find a less retarded signature + public static readonly Pattern TaskSchedulerSignature = new( + "55 8B EC 64 A1 00 00 00 00 6A FF 68 ?? ?? ?? ?? 50 64 89 25 00 00 00 00 83 EC 14 64 A1 2C 00 00 00 8B 08 A1 ?? ?? ?? ?? 3B 81 08 00 00 00 7F 29 A1 ?? ?? ?? ?? 8B 4D F4 64 89 0D 00 00 00 00 8B E5 5D C3 8D 4D E4 E8 ?? ?? ?? ?? 68 ?? ?? ?? ?? 8D 45 E4 50 E8 ?? ?? ?? ?? 68 ?? ?? ?? ?? E8 ?? ?? ?? ?? 83 C4 04 83 3D ?? ?? ?? ?? ?? 75 C1 68"); + + // UserId static global variable + // string: "PlayerId=%llu\n", last instruction + public static readonly Pattern UserIdSignature = + new( + "FF 35 ?? ?? ?? ?? 68 ?? ?? ?? ?? 68 00 04 00 00 50 E8 ?? ?? ?? ?? 8D 8D D4 FA FF FF 83 C4 14 8D 51 01 8A 01"); +} \ No newline at end of file diff --git a/src/Starlight/PostLaunch/TaskScheduler.cs b/src/Starlight/PostLaunch/TaskScheduler.cs new file mode 100644 index 0000000..ea39e34 --- /dev/null +++ b/src/Starlight/PostLaunch/TaskScheduler.cs @@ -0,0 +1,16 @@ +using HackerFramework; + +namespace Starlight.PostLaunch; + +public class TaskScheduler +{ + internal uint BaseAddress; + + internal ClientInstance Instance; + + public void WriteDouble(uint offset, double value) + { + var addr = BaseAddress + offset; + Instance.Rbx.WriteDouble(addr, value); + } +} \ No newline at end of file diff --git a/src/Starlight/RbxApp/AppModException.cs b/src/Starlight/RbxApp/AppModException.cs deleted file mode 100644 index c3648a1..0000000 --- a/src/Starlight/RbxApp/AppModException.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; - -namespace Starlight.RbxApp -{ - public class AppModException : Exception - { - public AppModException(string message) : base(message) { } - } -} diff --git a/src/Starlight/RbxApp/RbxInstance.cs b/src/Starlight/RbxApp/RbxInstance.cs deleted file mode 100644 index 1ea8ec9..0000000 --- a/src/Starlight/RbxApp/RbxInstance.cs +++ /dev/null @@ -1,119 +0,0 @@ -using HackerFramework; -using log4net; -using System; -using System.Diagnostics; -using System.Threading; -using System.Threading.Tasks; - -namespace Starlight.RbxApp -{ - public class RbxInstance - { - // ReSharper disable once PossibleNullReferenceException - internal static readonly ILog Log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); - - public readonly Process Proc; - - public readonly Target Rbx; - - long _userId; - public long GetUserId() - { - if (_userId != 0) - return _userId; - - var results = Rbx.FindPattern(RobloxData.UserIdSignature); - if (results.Count is 0 or > 1) - { - var ex = new AppModException("Failed to find UserId."); - Log.Fatal("UserId signature scan failed. Signature update required.", ex); - throw ex; - } - - var userIdAddr = Rbx.ReadPointer(results[0] + RobloxData.UserIdOffset); - long userId; - while ((userId = Rbx.ReadLong(userIdAddr)) == 0 || userId == -1) - Thread.Sleep(100); - - _userId = userId; - return userId; - } - - public async Task GetUserIdAsync() => - await Task.Run(GetUserId); - - TaskScheduler _taskScheduler; - public TaskScheduler GetTaskScheduler() - { - if (_taskScheduler is not null) - return _taskScheduler; - - var results = Rbx.FindPattern(RobloxData.TaskSchedulerSignature); - if (results.Count is 0 or > 1) - { - var ex = new AppModException("Failed to find TaskScheduler."); - Log.Fatal("TaskScheduler signature scan failed. Signature update required.", ex); - throw ex; - } - - var sched = new TaskScheduler { Instance = this }; - var singletonPtr = Rbx.ReadPointer(results[0] + RobloxData.TaskSchedulerOffset); - while ((sched.BaseAddress = Rbx.ReadPointer(singletonPtr)) == 0) - Thread.Sleep(100); - - _taskScheduler = sched; - return sched; - } - - public async Task GetTaskSchedulerAsync() => - await Task.Run(GetTaskScheduler); - - uint _frameDelayOff; - public void SetFrameDelay(double delay) - { - var sched = GetTaskScheduler(); - if (_frameDelayOff == 0) - { - const double defaultDelay = 1.0d / 60.0d; // 60hz - for (var off = sched.BaseAddress; off < sched.BaseAddress + 0x1000 - sizeof(double); off += 4) - { - var diff = Rbx.ReadDouble(off) - defaultDelay; - if (!(Math.Abs(diff) < 0.001)) - continue; - - _frameDelayOff = off - sched.BaseAddress; - break; - } - - if (_frameDelayOff == 0) - { - var ex = new AppModException("Failed to find FrameDelay."); - Log.Fatal("FrameDelay offset scan failed. Possibly an invalid TaskScheduler object. Signature update MAY be required.", ex); - throw ex; - } - } - - sched.WriteDouble(_frameDelayOff, delay); - } - - public async Task SetFrameDelayAsync(double delay) => - await Task.Run(() => SetFrameDelay(delay)); - - public RbxInstance(int procId) - { - Proc = Process.GetProcessById(procId); - if (Proc is null || Proc.HasExited) - { - var ex = new AppModException("Failed to get Roblox process."); - Log.Fatal("Roblox prematurely exited.", ex); - throw ex; - } - Rbx = new Target(Proc); - } - - ~RbxInstance() - { - Rbx.Dispose(); - } - } -} diff --git a/src/Starlight/RbxApp/RobloxData.cs b/src/Starlight/RbxApp/RobloxData.cs deleted file mode 100644 index 0b087c1..0000000 --- a/src/Starlight/RbxApp/RobloxData.cs +++ /dev/null @@ -1,22 +0,0 @@ -using HackerFramework; - -namespace Starlight.RbxApp -{ - // The signatures and offsets stored here may need to be - // updated if Roblox chooses to change their code, which - // is highly unlikely to happen. - - internal class RobloxData - { - // TaskScheduler::singleton - // string: "Load ClientAppSettings", last four calls - // todo: Maybe find a less retarded signature - public static readonly Pattern TaskSchedulerSignature = new("55 8B EC 64 A1 00 00 00 00 6A FF 68 ?? ?? ?? ?? 50 64 89 25 00 00 00 00 83 EC 14 64 A1 2C 00 00 00 8B 08 A1 ?? ?? ?? ?? 3B 81 08 00 00 00 7F 29 A1 ?? ?? ?? ?? 8B 4D F4 64 89 0D 00 00 00 00 8B E5 5D C3 8D 4D E4 E8 ?? ?? ?? ?? 68 ?? ?? ?? ?? 8D 45 E4 50 E8 ?? ?? ?? ?? 68 ?? ?? ?? ?? E8 ?? ?? ?? ?? 83 C4 04 83 3D ?? ?? ?? ?? ?? 75 C1 68"); - public const uint TaskSchedulerOffset = 49; // mov eax, dword_xxxxxxxx, instruction should appear twice for same func - - // UserId static global variable - // string: "PlayerId=%llu\n", last instruction - public static readonly Pattern UserIdSignature = new("FF 35 ?? ?? ?? ?? 68 ?? ?? ?? ?? 68 00 04 00 00 50 E8 ?? ?? ?? ?? 8D 8D D4 FA FF FF 83 C4 14 8D 51 01 8A 01"); - public const uint UserIdOffset = 2; // skips to rel32 of push instruction - } -} diff --git a/src/Starlight/RbxApp/TaskScheduler.cs b/src/Starlight/RbxApp/TaskScheduler.cs deleted file mode 100644 index 2d481e0..0000000 --- a/src/Starlight/RbxApp/TaskScheduler.cs +++ /dev/null @@ -1,17 +0,0 @@ -using HackerFramework; - -namespace Starlight.RbxApp -{ - public class TaskScheduler - { - internal RbxInstance Instance; - - internal uint BaseAddress; - - public void WriteDouble(uint offset, double value) - { - var addr = BaseAddress + offset; - Instance.Rbx.WriteDouble(addr, value); - } - } -} diff --git a/src/Starlight/SchemeLaunch/ParseResultFlags.cs b/src/Starlight/SchemeLaunch/ParseResultFlags.cs new file mode 100644 index 0000000..a61c827 --- /dev/null +++ b/src/Starlight/SchemeLaunch/ParseResultFlags.cs @@ -0,0 +1,23 @@ +using System; + +namespace Starlight.SchemeLaunch; + +[Flags] +internal enum ParseResultFlags +{ + TicketExists = 0x0, + RequestExists = 0x2, + RequestParsed = 0x4, + LaunchTimeExists = 0x8, + LaunchTimeParsed = 0x10, + TrackerIdExists = 0x20, + TrackerIdParsed = 0x40, + RobloxLocaleExists = 0x80, + RobloxLocaleParsed = 0x100, + GameLocaleExists = 0x200, + GameLocaleParsed = 0x300, + + Success = TicketExists | RequestExists | RequestParsed | LaunchTimeExists + | LaunchTimeParsed | TrackerIdExists | TrackerIdParsed | RobloxLocaleExists + | RobloxLocaleParsed | GameLocaleExists | GameLocaleParsed +} \ No newline at end of file diff --git a/src/Starlight/SchemeLaunch/Scheme.cs b/src/Starlight/SchemeLaunch/Scheme.cs new file mode 100644 index 0000000..c6fb3a5 --- /dev/null +++ b/src/Starlight/SchemeLaunch/Scheme.cs @@ -0,0 +1,186 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using System.Web; +using log4net; +using Microsoft.Win32; +using Starlight.Apis.JoinGame; +using Starlight.Bootstrap; +using Starlight.Except; +using Starlight.Launch; +using Starlight.Misc; +using Starlight.PostLaunch; + +namespace Starlight.SchemeLaunch; + +public class Scheme +{ + // ReSharper disable once PossibleNullReferenceException + internal static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + + internal static IReadOnlyDictionary ParseRaw(string payload) + { + try + { + var split = HttpUtility.UrlDecode(payload).Split(' '); + return split.Select(t => t.Split(':')) + .ToDictionary(pair => pair[0], pair => string.Join(":", pair.Skip(1))); + } + catch + { + Log.Error("Failed to parse raw payload data."); + return null; + } + } + + /// + /// Parse a Roblox launch scheme payload into + /// + /// The raw payload to use. + /// A class representing the deserialized payload. + public static LaunchParams Parse(string rawArgs) + { + LaunchParams info = new(); + var args = ParseRaw(rawArgs); + ParseResultFlags result = 0; + + if (args.TryGetValue("gameinfo", out var ticket)) + { + result |= ParseResultFlags.TicketExists; + info.Ticket = ticket; + } + + if (args.TryGetValue("placelauncherurl", out var launchUrl)) + { + result |= ParseResultFlags.RequestExists; + if (Uri.TryCreate(launchUrl, UriKind.Absolute, out var launchUri)) + { + result |= ParseResultFlags.RequestParsed; + info.Request = new JoinRequest(launchUri); + } + } + + if (args.TryGetValue("launchtime", out var launchTimeStr)) + { + result |= ParseResultFlags.LaunchTimeExists; + if (long.TryParse(launchTimeStr, out var launchTime)) + { + result |= ParseResultFlags.LaunchTimeParsed; + info.LaunchTime = DateTimeOffset.FromUnixTimeMilliseconds(launchTime); + } + } + + if (args.TryGetValue("browsertrackerid", out var trackerIdStr)) + { + result |= ParseResultFlags.TrackerIdExists; + if (long.TryParse(trackerIdStr, out var trackerId)) + { + result |= ParseResultFlags.TrackerIdParsed; + info.TrackerId = trackerId; + } + } + + if (args.TryGetValue("robloxLocale", out var rbxLocaleStr)) + { + result |= ParseResultFlags.RobloxLocaleExists; + if (Utility.TryGetCultureInfo(rbxLocaleStr, out var rbxLocale)) + { + result |= ParseResultFlags.RobloxLocaleParsed; + info.RobloxLocale = rbxLocale; + } + } + + if (args.TryGetValue("gameLocale", out var gameLocaleStr)) + { + result |= ParseResultFlags.GameLocaleExists; + if (Utility.TryGetCultureInfo(gameLocaleStr, out var gameLocale)) + { + result |= ParseResultFlags.GameLocaleParsed; + info.GameLocale = gameLocale; + } + } + + if (result.HasFlag(ParseResultFlags.Success)) + return info; + + Log.Error($"Failed to parse scheme payload. Parse result: {result}"); + return null; + } + + /// + /// Launch Roblox using a launcher scheme payload. + /// + /// The raw payload data. + /// The extra Starlight launch parameters to use. + /// A returned by . + public static ClientInstance Launch(string args, IStarlightLaunchParams extras = null) + { + var parsed = Parse(args); + + if (parsed != null) + return Launcher.Launch(parsed, extras); + + var ex = new SchemeParseException("Failed to parse scheme payload."); + Log.Fatal("Failed to deserialize payload into LaunchParams.", ex); + throw ex; + } + + /// + /// Launch Roblox using a launcher scheme payload. + /// + /// The raw payload data. + /// The extra Starlight launch parameters to use. + /// A returned by . + public static async Task LaunchAsync(string args, IStarlightLaunchParams extras = null) => + await Task.Run(() => Launch(args, extras)); + + /// + /// Registers the Starlight scheme handler. + /// + /// The binary path to launch. + /// Any command line options to provide. + /// A boolean determining whether or not the function succeeded. + public static bool Hook(string launcherBin, string options = "") + { + try + { + using var registryKey = + Registry.CurrentUser.CreateSubKey("Software\\Classes\\roblox-player\\shell\\open\\command"); + registryKey?.SetValue(string.Empty, $"\"{launcherBin}\" {options}%1", RegistryValueKind.String); + return true; + } + catch + { + Log.Error("Failed to hook scheme."); + return false; + } + } + + /// + /// Unhook the scheme that Roblox uses to launch. + /// + /// A boolean determining whether or not the function succeeded. + public static bool Unhook() + { + try + { + var rbxBin = Bootstrapper.GetClients()[0].Player; + using var registryKey = + Registry.CurrentUser.CreateSubKey("Software\\Classes\\roblox-player\\shell\\open\\command"); + registryKey?.SetValue(string.Empty, $"\"{rbxBin}\" %1", RegistryValueKind.String); + return true; + } + catch (ClientNotFoundException) + { + Registry.CurrentUser.DeleteSubKeyTree("Software\\Classes\\roblox-player\\shell"); + return true; + } + catch + { + Log.Error("Failed to unhook scheme."); + return false; + } + } +} \ No newline at end of file diff --git a/src/Starlight/Starlight.csproj b/src/Starlight/Starlight.csproj index a724e5b..e4d3c9c 100644 --- a/src/Starlight/Starlight.csproj +++ b/src/Starlight/Starlight.csproj @@ -1,50 +1,54 @@  - - net48 - latest - x86 - bin\$(Configuration)\ - false - obj\ - False - A general-purpose Roblox library - Copyright (c) 2022 Substrant - x86 - - - portable - - - embedded - - - - tlbimp - 0 - 1 - f935dc20-1cf0-11d0-adb9-00c04fd58a0b - 0 - false - true - - - - - all - - - all - - - - - - - - - - - - - + + net48 + latest + x86 + bin\$(Configuration)\ + false + obj\ + False + A general-purpose Roblox library + Copyright (c) 2022 Substrant + x86 + + + portable + + + embedded + + + + tlbimp + 0 + 1 + f935dc20-1cf0-11d0-adb9-00c04fd58a0b + 0 + false + true + + + + + all + + + all + + + + + + + + + + + + D:\Exploits\Synapse X\bin\sxlib\sxlib.dll + + + + + \ No newline at end of file diff --git a/src/Starlight/app.config b/src/Starlight/app.config index 22d5f49..379f1e2 100644 --- a/src/Starlight/app.config +++ b/src/Starlight/app.config @@ -1,15 +1,16 @@  + - - - - - - - - - - - - + + + + + + + + + + + + \ No newline at end of file