├── docs ├── login.PNG ├── Version6_10.png └── screenshot.png ├── libs └── ModniteServer.API.Core.dll ├── ModniteServer ├── Inconsolata-Regular.ttf ├── App.xaml.cs ├── App.config ├── packages.config ├── Properties │ ├── Settings.settings │ ├── AssemblyInfo.cs │ ├── Settings.Designer.cs │ ├── Resources.Designer.cs │ └── Resources.resx ├── IUserCommand.cs ├── App.xaml ├── Controls │ ├── LogStreamSink.cs │ ├── LogStream.xaml │ ├── LogStreamEntry.xaml.cs │ ├── LogStream.xaml.cs │ └── LogStreamEntry.xaml ├── Commands │ ├── UpdateCommand.cs │ ├── CreateAccountCommand.cs │ ├── DisplayNameCommand.cs │ ├── GetItemsCommand.cs │ └── GiveItemCommand.cs ├── Views │ ├── MainWindow.xaml.cs │ └── MainWindow.xaml ├── CommandManager.cs ├── ObservableObject.cs ├── ViewModels │ └── MainViewModel.cs └── ModniteServer.csproj ├── ModniteServer.Api ├── Assets │ └── ClientSettings.Sav ├── Controllers │ ├── AffiliateController.cs │ ├── TournamentController.cs │ ├── TelemetryController.cs │ ├── LightswitchController.cs │ ├── PrivacyController.cs │ ├── WaitingRoomController.cs │ ├── StorefrontController.cs │ ├── CloudStorageController.cs │ ├── AccessController.cs │ ├── AccountController.cs │ ├── FriendsController.cs │ ├── CalendarController.cs │ ├── MatchmakingController.cs │ ├── CustomizationController.cs │ ├── ClientQuestController.cs │ ├── ContentController.cs │ ├── OAuthController.cs │ └── ProfileController.cs ├── OAuth │ ├── OAuthToken.cs │ └── OAuthManager.cs ├── Controller.cs ├── ModniteServer.API.csproj ├── Accounts │ ├── Account.cs │ └── AccountManager.cs └── ApiConfig.cs ├── ModniteServer.Services ├── ModniteServer.Services.csproj ├── Websockets │ ├── WebsocketMessageReceivedEventArgs.cs │ ├── WebsocketNewConnectionEventArgs.cs │ ├── WebsocketMessage.cs │ └── WebsocketServer.cs ├── Xmpp │ ├── XmppClient.cs │ └── XmppServer.cs └── Matchmaker │ └── MatchmakerServer.cs ├── LICENSE ├── ModniteServer.sln ├── README.md └── .gitignore /docs/login.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msx752/ModniteServer/HEAD/docs/login.PNG -------------------------------------------------------------------------------- /docs/Version6_10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msx752/ModniteServer/HEAD/docs/Version6_10.png -------------------------------------------------------------------------------- /docs/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msx752/ModniteServer/HEAD/docs/screenshot.png -------------------------------------------------------------------------------- /libs/ModniteServer.API.Core.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msx752/ModniteServer/HEAD/libs/ModniteServer.API.Core.dll -------------------------------------------------------------------------------- /ModniteServer/Inconsolata-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msx752/ModniteServer/HEAD/ModniteServer/Inconsolata-Regular.ttf -------------------------------------------------------------------------------- /ModniteServer.Api/Assets/ClientSettings.Sav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msx752/ModniteServer/HEAD/ModniteServer.Api/Assets/ClientSettings.Sav -------------------------------------------------------------------------------- /ModniteServer/App.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | namespace ModniteServer 4 | { 5 | public partial class App : Application 6 | { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /ModniteServer/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /ModniteServer/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /ModniteServer/Properties/Settings.settings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /ModniteServer/IUserCommand.cs: -------------------------------------------------------------------------------- 1 | namespace ModniteServer 2 | { 3 | public interface IUserCommand 4 | { 5 | string Description { get; } 6 | 7 | string ExampleArgs { get; } 8 | 9 | string Args { get; } 10 | 11 | void Handle(string[] args); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ModniteServer/App.xaml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /ModniteServer.Api/Controllers/AffiliateController.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | 3 | namespace ModniteServer.API.Controllers 4 | { 5 | public sealed class AffiliateController : Controller 6 | { 7 | [Route("GET", "/affiliate/api/public/affiliates/slug/*")] 8 | public void CheckIfAffiliateExists() 9 | { 10 | string affiliateName = Request.Url.Segments.Last(); 11 | 12 | Response.StatusCode = 404; 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /ModniteServer.Services/ModniteServer.Services.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 7.2 6 | ModniteServer 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ModniteServer/Controls/LogStreamSink.cs: -------------------------------------------------------------------------------- 1 | using Serilog.Core; 2 | using Serilog.Events; 3 | 4 | namespace ModniteServer.Controls 5 | { 6 | public class LogStreamSink : ILogEventSink 7 | { 8 | public LogStreamSink(LogStream logStream) 9 | { 10 | LogStream = logStream; 11 | } 12 | 13 | public LogStream LogStream { get; } 14 | 15 | public void Emit(LogEvent logEvent) 16 | { 17 | LogStream.AppendEvent(logEvent); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /ModniteServer/Commands/UpdateCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace ModniteServer.Commands 4 | { 5 | public sealed class UpdateCommand : IUserCommand 6 | { 7 | public string Description => "Gets the latest version of Modnite Server"; 8 | public string Args => ""; 9 | public string ExampleArgs => ""; 10 | 11 | public void Handle(string[] args) 12 | { 13 | Process.Start("https://github.com/ModniteNet/ModniteServer/releases"); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /ModniteServer.Services/Websockets/WebsocketMessageReceivedEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Sockets; 3 | 4 | namespace ModniteServer.Websockets 5 | { 6 | internal sealed class WebsocketMessageReceivedEventArgs : EventArgs 7 | { 8 | public WebsocketMessageReceivedEventArgs(Socket socket, WebsocketMessage message) 9 | { 10 | Socket = socket; 11 | Message = message; 12 | } 13 | 14 | public Socket Socket { get; } 15 | 16 | public WebsocketMessage Message { get; } 17 | } 18 | } -------------------------------------------------------------------------------- /ModniteServer.Services/Websockets/WebsocketNewConnectionEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Sockets; 3 | 4 | namespace ModniteServer.Websockets 5 | { 6 | internal sealed class WebsocketMessageNewConnectionEventArgs : EventArgs 7 | { 8 | public WebsocketMessageNewConnectionEventArgs(Socket socket, string authorization) 9 | { 10 | Socket = socket; 11 | Authorization = authorization; 12 | } 13 | 14 | public Socket Socket { get; } 15 | 16 | public string Authorization { get; } 17 | } 18 | } -------------------------------------------------------------------------------- /ModniteServer.Api/OAuth/OAuthToken.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ModniteServer.API.OAuth 4 | { 5 | /// 6 | /// Represents an OAuth token. 7 | /// 8 | internal struct OAuthToken 9 | { 10 | public OAuthToken(string token, int expiresIn) 11 | { 12 | Token = token; 13 | ExpiresIn = expiresIn; 14 | ExpiresAt = DateTime.UtcNow.Add(TimeSpan.FromSeconds(expiresIn)); 15 | } 16 | 17 | public string Token { get; } 18 | 19 | public int ExpiresIn { get; } 20 | 21 | public DateTime ExpiresAt { get; } 22 | } 23 | } -------------------------------------------------------------------------------- /ModniteServer/Controls/LogStream.xaml: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ModniteServer.Api/Controller.cs: -------------------------------------------------------------------------------- 1 | using ModniteServer.API.OAuth; 2 | 3 | namespace ModniteServer.API 4 | { 5 | /// 6 | /// Provides a base implementation of an API controller. 7 | /// 8 | public abstract class Controller : ControllerCore 9 | { 10 | protected bool Authorize() 11 | { 12 | return true; 13 | 14 | //string authorization = Request.Headers["Authorization"]; 15 | 16 | //if (authorization != null && authorization.StartsWith("bearer ")) 17 | //{ 18 | // string token = authorization.Split(' ')[1]; 19 | 20 | // if (OAuthManager.IsTokenValid(token)) 21 | // return true; 22 | //} 23 | 24 | //Response.StatusCode = 403; 25 | //return false; 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /ModniteServer/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | // Modnite Server - Copyright © 2018 Modnite Contributors 2 | // Licensed under the Affero General Public License 3 | 4 | using System.Reflection; 5 | using System.Runtime.InteropServices; 6 | using System.Windows; 7 | 8 | [assembly: AssemblyTitle("ModniteServer")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("https://modnite.net")] 12 | [assembly: AssemblyProduct("ModniteServer")] 13 | [assembly: AssemblyCopyright("Copyright © 2018 Modnite Contributors")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | [assembly: AssemblyVersion("1.0.0.0")] 17 | [assembly: AssemblyFileVersion("1.0.0.0")] 18 | 19 | [assembly: ComVisible(false)] 20 | 21 | [assembly: ThemeInfo(ResourceDictionaryLocation.None, ResourceDictionaryLocation.SourceAssembly)] -------------------------------------------------------------------------------- /ModniteServer.Api/ModniteServer.API.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 7.2 6 | 7 | 8 | 9 | TRACE 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | ..\libs\ModniteServer.API.Core.dll 20 | 21 | 22 | 23 | 24 | 25 | Always 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /ModniteServer.Api/Controllers/TournamentController.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace ModniteServer.API.Controllers 6 | { 7 | public sealed class TournamentController : Controller 8 | { 9 | [Route("GET", "/fortnite/api/game/v2/events/tournamentandhistory/*/*/*")] 10 | public void GetTournaments() 11 | { 12 | string accountId = Request.Url.Segments[Request.Url.Segments.Length - 3].Replace("/", ""); 13 | string region = Request.Url.Segments[Request.Url.Segments.Length - 2].Replace("/", ""); 14 | string clientType = Request.Url.Segments.Last(); 15 | 16 | var response = new 17 | { 18 | eventTournaments = new List(), 19 | eventWindowHistories = new List() 20 | }; 21 | 22 | Response.StatusCode = 200; 23 | Response.ContentType = "application/json"; 24 | Response.Write(JsonConvert.SerializeObject(response)); 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Modnite 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ModniteServer/Properties/Settings.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace ModniteServer.Properties 12 | { 13 | 14 | 15 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 16 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")] 17 | internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase 18 | { 19 | 20 | private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); 21 | 22 | public static Settings Default 23 | { 24 | get 25 | { 26 | return defaultInstance; 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /ModniteServer.Api/Controllers/TelemetryController.cs: -------------------------------------------------------------------------------- 1 | using Serilog; 2 | using System.Text; 3 | 4 | namespace ModniteServer.API.Controllers 5 | { 6 | public sealed class TelemetryController : Controller 7 | { 8 | [Route("POST", "/datarouter/api/v1/public/data")] 9 | public void ReceiveEvents() 10 | { 11 | Query.TryGetValue("SessionID", out string sessionId); 12 | Query.TryGetValue("AppID", out string appId); 13 | Query.TryGetValue("AppVersion", out string appVersion); 14 | Query.TryGetValue("UserID", out string userId); 15 | Query.TryGetValue("AppEnvironment", out string appEnvironment); 16 | Query.TryGetValue("UploadType", out string uploadType); 17 | 18 | byte[] body = new byte[Request.ContentLength64]; 19 | Request.InputStream.Read(body, 0, body.Length); 20 | string bodyText = Encoding.UTF8.GetString(body); 21 | 22 | Log.Information("Telemetry data received {SessionId}{AppId}{AppVersion}{UserId}{AppEnvironment}{UploadType}{Data}", sessionId, appId, appVersion, userId, appEnvironment, uploadType, bodyText); 23 | 24 | Response.StatusCode = 200; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /ModniteServer.Api/Accounts/Account.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace ModniteServer.API.Accounts 5 | { 6 | public class Account 7 | { 8 | public Account() 9 | { 10 | Country = "US"; 11 | PreferredLanguage = "en"; 12 | DisplayNameHistory = new List(); 13 | } 14 | 15 | public string AccountId { get; set; } 16 | 17 | public string Email { get; set; } 18 | 19 | public string PasswordHash { get; set; } 20 | 21 | public string DisplayName { get; set; } 22 | 23 | public List DisplayNameHistory { get; set; } 24 | 25 | public int FailedLoginAttempts { get; set; } 26 | 27 | public string FirstName { get; set; } 28 | 29 | public string LastName { get; set; } 30 | 31 | public DateTime LastLogin { get; set; } 32 | 33 | public string Country { get; set; } 34 | 35 | public string PreferredLanguage { get; set; } 36 | 37 | public bool IsBanned { get; set; } 38 | 39 | public HashSet AthenaItems { get; set; } 40 | 41 | public HashSet CoreItems { get; set; } 42 | 43 | public Dictionary EquippedItems { get; set; } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /ModniteServer/Views/MainWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | using ModniteServer.API.Accounts; 2 | using ModniteServer.Controls; 3 | using ModniteServer.ViewModels; 4 | using Serilog; 5 | using System.ComponentModel; 6 | using System.Windows; 7 | using System.Windows.Input; 8 | 9 | namespace ModniteServer.Views 10 | { 11 | public partial class MainWindow : Window 12 | { 13 | public MainWindow() 14 | { 15 | InitializeComponent(); 16 | Log.Logger = new LoggerConfiguration().WriteTo.Sink(new LogStreamSink(this.logStream)).CreateLogger(); 17 | DataContext = ViewModel = new MainViewModel(); 18 | 19 | this.commandTextBox.GotFocus += delegate { this.commandTextBox.Tag = ""; }; 20 | this.commandTextBox.LostFocus += delegate { this.commandTextBox.Tag = "Enter command"; }; 21 | this.commandTextBox.KeyDown += (sender, args) => 22 | { 23 | if (args.Key == Key.Enter) 24 | ViewModel.InvokeCommand(); 25 | }; 26 | 27 | this.Closing += OnMainWindowClosing; 28 | } 29 | 30 | public MainViewModel ViewModel { get; } 31 | 32 | void OnMainWindowClosing(object sender, CancelEventArgs e) 33 | { 34 | AccountManager.SaveAccounts(); 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /ModniteServer.Api/Controllers/LightswitchController.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace ModniteServer.API.Controllers 4 | { 5 | public sealed class LightswitchController : Controller 6 | { 7 | /// 8 | /// Checks the status of Fortnite services. In our case, we're always up when the server is. ;) 9 | /// 10 | [Route("GET", "/lightswitch/api/service/bulk/status")] 11 | public void GetStatus() 12 | { 13 | var response = new[] 14 | { 15 | new 16 | { 17 | serviceInstanceId = "fortnite", 18 | status = "UP", 19 | message = "Down for maintenance", 20 | maintenanceUrl = (string)null, 21 | overrideCatalogIds = new string[0], 22 | allowedActions = new string[0], 23 | banned = false, // we check for this in OAuthController 24 | launcherInfoDTO = new 25 | { 26 | appName = "Fortnite", 27 | catalogItemId = "", 28 | @namespace = "fn" 29 | } 30 | } 31 | }; 32 | 33 | Response.StatusCode = 200; 34 | Response.ContentType = "application/json"; 35 | Response.Write(JsonConvert.SerializeObject(response)); 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /ModniteServer/Commands/CreateAccountCommand.cs: -------------------------------------------------------------------------------- 1 | using ModniteServer.API.Accounts; 2 | using Serilog; 3 | using System.Security.Cryptography; 4 | using System.Text; 5 | 6 | namespace ModniteServer.Commands 7 | { 8 | /// 9 | /// User command for creating a new account. 10 | /// 11 | public sealed class CreateAccountCommand : IUserCommand 12 | { 13 | public string Description => "Creates an account"; 14 | public string Args => " "; 15 | public string ExampleArgs => "player123 hunter2"; 16 | 17 | public void Handle(string[] args) 18 | { 19 | if (args.Length > 1) 20 | { 21 | if (AccountManager.AccountExists(args[0])) 22 | { 23 | Log.Error($"Account '{args[0]}' already exists"); 24 | return; 25 | } 26 | 27 | string email = args[0] + "@modnite.net"; 28 | 29 | string passwordHash; 30 | using (var sha256 = new SHA256Managed()) 31 | { 32 | byte[] hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(args[1])); 33 | var hashString = new StringBuilder(); 34 | foreach (byte b in hash) 35 | { 36 | hashString.AppendFormat("{0:x2}", b); 37 | } 38 | passwordHash = hashString.ToString(); 39 | } 40 | 41 | AccountManager.CreateAccount(args[0] + "@modnite.net", passwordHash); 42 | } 43 | else 44 | { 45 | Log.Error("Invalid arguments"); 46 | } 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /ModniteServer/Commands/DisplayNameCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using ModniteServer.API.Accounts; 3 | using Serilog; 4 | 5 | namespace ModniteServer.Commands 6 | { 7 | /// 8 | /// User command to change the display name for an account. 9 | /// 10 | public sealed class DisplayNameCommand : IUserCommand 11 | { 12 | public string Description => "Sets the display name for an account."; 13 | public string ExampleArgs => "player123 Cool name"; 14 | public string Args => " "; 15 | 16 | public void Handle(string[] args) 17 | { 18 | if (args.Length >= 2) 19 | { 20 | if (AccountManager.AccountExists(args[0])) 21 | { 22 | var account = AccountManager.GetAccount(args[0]); 23 | string newName = string.Join(" ", args.Skip(1)); 24 | 25 | if (account.DisplayName == newName) 26 | { 27 | Log.Error($"Account '{args[0]}' already has display name '{account.DisplayName}'"); 28 | } 29 | else 30 | { 31 | account.DisplayName = newName; 32 | 33 | Log.Information($"Set display name for account '{args[0]}' to '{newName}'"); 34 | AccountManager.SaveAccounts(); 35 | } 36 | } 37 | else 38 | { 39 | Log.Error($"Account '{args[0]}' does not exist"); 40 | } 41 | } 42 | else 43 | { 44 | Log.Error("Invalid arguments"); 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /ModniteServer.Api/Controllers/PrivacyController.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Newtonsoft.Json.Linq; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace ModniteServer.API.Controllers 7 | { 8 | public sealed class PrivacyController : Controller 9 | { 10 | [Route("GET", "/fortnite/api/game/v2/privacy/account/*")] 11 | public void GetPrivacySetting() 12 | { 13 | string accountId = Request.Url.Segments.Last(); 14 | 15 | var response = new 16 | { 17 | accountId, 18 | optOutOfPublicLeaderboards = true 19 | }; 20 | 21 | Response.StatusCode = 200; 22 | Response.ContentType = "application/json"; 23 | Response.Write(JsonConvert.SerializeObject(response)); 24 | } 25 | 26 | [Route("POST", "/fortnite/api/game/v2/privacy/account/*")] 27 | public void SetPrivacySetting() 28 | { 29 | string accountId = Request.Url.Segments.Last(); 30 | 31 | byte[] buffer = new byte[Request.ContentLength64]; 32 | Request.InputStream.Read(buffer, 0, buffer.Length); 33 | 34 | var request = JObject.Parse(Encoding.UTF8.GetString(buffer)); 35 | 36 | bool optOutOfPublicLeaderboards = (bool)request["optOutOfPublicLeaderboards"]; 37 | bool optOutOfFriendsLeaderboards = (bool)request["optOutOfFriendsLeaderboards"]; 38 | 39 | var response = new 40 | { 41 | accountId, 42 | optOutOfPublicLeaderboards 43 | }; 44 | 45 | Response.StatusCode = 200; 46 | Response.ContentType = "application/json"; 47 | Response.Write(JsonConvert.SerializeObject(response)); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /ModniteServer/Controls/LogStreamEntry.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.ObjectModel; 3 | using System.Windows; 4 | using System.Windows.Controls; 5 | 6 | namespace ModniteServer.Controls 7 | { 8 | public partial class LogStreamEntry : UserControl 9 | { 10 | public static readonly DependencyProperty TextProperty 11 | = DependencyProperty.Register(nameof(Text), typeof(string), typeof(LogStreamEntry)); 12 | 13 | public static readonly DependencyProperty PropertiesProperty 14 | = DependencyProperty.Register(nameof(Properties), typeof(ObservableCollection<(string, string)>), typeof(LogStreamEntry)); 15 | 16 | public LogStreamEntry() 17 | { 18 | InitializeComponent(); 19 | DataContext = this; 20 | } 21 | 22 | public string Text 23 | { 24 | get { return (string)GetValue(TextProperty); } 25 | set { SetValue(TextProperty, value); } 26 | } 27 | 28 | public ObservableCollection<(string Key, string Value)> Properties 29 | { 30 | get { return (ObservableCollection<(string, string)>)GetValue(PropertiesProperty); } 31 | set 32 | { 33 | SetValue(PropertiesProperty, value); 34 | 35 | foreach (var (Key, Value) in Properties) 36 | { 37 | var propertyItem = new TreeViewItem 38 | { 39 | HeaderTemplate = (DataTemplate)Resources["LogStreamTreeViewItemHeaderTemplate"], 40 | Style = (Style)Resources["TreeViewItemStyle"], 41 | 42 | // ValueTuple only has fields, but Tuple has properties which is required for binding. 43 | Header = new Tuple(Key + ":", Value) 44 | }; 45 | this.treeRoot.Items.Add(propertyItem); 46 | } 47 | } 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /ModniteServer/Commands/GetItemsCommand.cs: -------------------------------------------------------------------------------- 1 | using ModniteServer.API.Accounts; 2 | using Serilog; 3 | 4 | namespace ModniteServer.Commands 5 | { 6 | /// 7 | /// User command for listing all items an account has. 8 | /// 9 | public sealed class GetItemsCommand : IUserCommand 10 | { 11 | public string Description => "Lists all items in an account. For cosmetics, use profile 'athena'. For banners, use profile 'common_core'"; 12 | public string ExampleArgs => "player123 athena"; 13 | public string Args => " "; 14 | 15 | public void Handle(string[] args) 16 | { 17 | if (args.Length > 1) 18 | { 19 | if (AccountManager.AccountExists(args[0])) 20 | { 21 | var account = AccountManager.GetAccount(args[0]); 22 | 23 | if (args[1] == "athena") 24 | { 25 | Log.Information($"Items in '{args[0]}' for profile '{args[1]}':"); 26 | foreach (string item in account.AthenaItems) 27 | { 28 | Log.Information(" " + item); 29 | } 30 | } 31 | else if (args[1] == "common_core") 32 | { 33 | Log.Information($"Items in '{args[0]}' for profile '{args[1]}':"); 34 | foreach (string item in account.CoreItems) 35 | { 36 | Log.Information(" " + item); 37 | } 38 | } 39 | else 40 | { 41 | Log.Error($"Invalid profile '{args[1]}'"); 42 | } 43 | } 44 | else 45 | { 46 | Log.Error($"Account '{args[0]}' does not exist"); 47 | } 48 | } 49 | else 50 | { 51 | Log.Error("Invalid arguments"); 52 | } 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /ModniteServer.Api/OAuth/OAuthManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Security.Cryptography; 4 | 5 | namespace ModniteServer.API.OAuth 6 | { 7 | // TODO: Save sessions to a file 8 | 9 | /// 10 | /// Creates, manages, and destroys OAuth tokens. 11 | /// 12 | internal static class OAuthManager 13 | { 14 | static OAuthManager() 15 | { 16 | Tokens = new Dictionary(); 17 | } 18 | 19 | private static Dictionary Tokens { get; set; } 20 | 21 | /// 22 | /// Checks whether the given token is valid and has not expired. 23 | /// 24 | /// The token to validate. 25 | /// true if the token is valid; Otherwise, false. 26 | public static bool IsTokenValid(string token) 27 | { 28 | if (!Tokens.ContainsKey(token)) 29 | return false; 30 | 31 | return Tokens[token].ExpiresAt > DateTime.UtcNow; 32 | } 33 | 34 | /// 35 | /// Invalidates the given token. 36 | /// 37 | /// The token to invalidate. 38 | public static void DestroyToken(string token) 39 | { 40 | Tokens.Remove(token); 41 | } 42 | 43 | /// 44 | /// Creates a new token with the specified expiration time. 45 | /// 46 | /// The duration (in seconds) this token will be valid for. 47 | /// A valid OAuth token. 48 | public static OAuthToken CreateToken(int expiresInSeconds) 49 | { 50 | byte[] tokenRaw = new byte[20]; 51 | 52 | using (var rng = new RNGCryptoServiceProvider()) 53 | rng.GetNonZeroBytes(tokenRaw); 54 | 55 | string tokenStr = BitConverter.ToString(tokenRaw).Replace("-", "").ToLowerInvariant(); 56 | var token = new OAuthToken(tokenStr, expiresInSeconds); 57 | Tokens.Add(tokenStr, token); 58 | return token; 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /ModniteServer.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.28010.2046 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModniteServer", "ModniteServer\ModniteServer.csproj", "{21EE4C5E-3C46-4885-9D30-CE1B497F0908}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ModniteServer.API", "ModniteServer.Api\ModniteServer.API.csproj", "{8AA47228-BA31-484F-ABB6-C91B5E3EE5BE}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ModniteServer.Services", "ModniteServer.Services\ModniteServer.Services.csproj", "{6959E822-FE2F-4D13-B3EF-2AA7C8D58EA9}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Release|Any CPU = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {21EE4C5E-3C46-4885-9D30-CE1B497F0908}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {21EE4C5E-3C46-4885-9D30-CE1B497F0908}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {21EE4C5E-3C46-4885-9D30-CE1B497F0908}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {21EE4C5E-3C46-4885-9D30-CE1B497F0908}.Release|Any CPU.Build.0 = Release|Any CPU 22 | {8AA47228-BA31-484F-ABB6-C91B5E3EE5BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {8AA47228-BA31-484F-ABB6-C91B5E3EE5BE}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {8AA47228-BA31-484F-ABB6-C91B5E3EE5BE}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {8AA47228-BA31-484F-ABB6-C91B5E3EE5BE}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {6959E822-FE2F-4D13-B3EF-2AA7C8D58EA9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {6959E822-FE2F-4D13-B3EF-2AA7C8D58EA9}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {6959E822-FE2F-4D13-B3EF-2AA7C8D58EA9}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {6959E822-FE2F-4D13-B3EF-2AA7C8D58EA9}.Release|Any CPU.Build.0 = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | GlobalSection(ExtensibilityGlobals) = postSolution 35 | SolutionGuid = {7D4FDCB0-DD8B-4268-B83E-90F149BBE1DA} 36 | EndGlobalSection 37 | EndGlobal 38 | -------------------------------------------------------------------------------- /ModniteServer/CommandManager.cs: -------------------------------------------------------------------------------- 1 | using ModniteServer.Commands; 2 | using Serilog; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace ModniteServer 7 | { 8 | public static class CommandManager 9 | { 10 | private static readonly Dictionary Handlers; 11 | 12 | static CommandManager() 13 | { 14 | Handlers = new Dictionary 15 | { 16 | ["commands"] = new ShowCommandsListCommand(), 17 | ["update"] = new UpdateCommand(), 18 | 19 | // Account Commands 20 | ["create"] = new CreateAccountCommand(), 21 | ["giveitem"] = new GiveItemCommand(), 22 | ["getitems"] = new GetItemsCommand(), 23 | ["displayname"] = new DisplayNameCommand(), 24 | }; 25 | } 26 | 27 | public static void InvokeCommand(string command) 28 | { 29 | if (string.IsNullOrWhiteSpace(command)) 30 | return; 31 | 32 | string[] commandParts = command.Split(' '); 33 | if (Handlers.ContainsKey(commandParts[0])) 34 | { 35 | Handlers[commandParts[0]].Handle(commandParts.Skip(1).ToArray()); 36 | } 37 | else 38 | { 39 | Log.Error($"Command '{command}' does not exist"); 40 | } 41 | } 42 | 43 | private class ShowCommandsListCommand : IUserCommand 44 | { 45 | public string Description => "Shows a list of available commands"; 46 | public string Args => ""; 47 | public string ExampleArgs => ""; 48 | 49 | public void Handle(string[] args) 50 | { 51 | Log.Information("AVAILABLE COMMANDS ------------------------"); 52 | foreach (var handler in Handlers) 53 | { 54 | if (handler.Value is ShowCommandsListCommand) 55 | continue; 56 | 57 | Log.Information($"{handler.Key} {handler.Value.Args}{{Description}}{{Example}}", handler.Value.Description, handler.Key + (string.IsNullOrEmpty(handler.Value.ExampleArgs) ? "" : " " + handler.Value.ExampleArgs)); 58 | } 59 | Log.Information("-------------------------------------------"); 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /ModniteServer.Api/Controllers/WaitingRoomController.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace ModniteServer.API.Controllers 4 | { 5 | public sealed class WaitingRoomController : Controller 6 | { 7 | /// 8 | /// Checks the queue to see if there's room for the client to join. 9 | /// 10 | [Route("GET", "/waitingroom/api/waitingroom")] 11 | public void CheckWaitQueue() 12 | { 13 | // There's no reason to have a wait queue in our case, but you can replace this with 14 | // your own logic if you want to limit the number of players on your server. 15 | bool hasQueue = false; 16 | 17 | if (hasQueue) 18 | { 19 | var response = new 20 | { 21 | retryTime = 10, // Seconds to wait before re-checking this API. 22 | expectedWait = 10 // Estimated time (in seconds) to display to the client 23 | }; 24 | 25 | Response.StatusCode = 200; 26 | Response.ContentType = "application/json"; 27 | Response.Write(JsonConvert.SerializeObject(response)); 28 | } 29 | else 30 | { 31 | // No content means no queue. The client is free to continue. 32 | Response.StatusCode = 204; 33 | } 34 | } 35 | 36 | /// 37 | /// Serves a static json with the queue info to the client. Normally this is hosted on AWS, 38 | /// but we don't want the client to query any URL outside our server. Occasionally, the 39 | /// client will ask for this file and we're providing it because it's better than a 404. 40 | /// 41 | [Route("GET", "/launcher-resources/waitingroom/Fortnite/retryconfig.json")] 42 | [Route("GET", "/launcher-resources/waitingroom/retryconfig.json")] 43 | public void GetRetryConfig() 44 | { 45 | var response = new 46 | { 47 | maxRetryCount = 2, 48 | retryInterval = 60, 49 | retryJitter = 10, 50 | failAction = "ABORT" // ABORT | CONTINUE 51 | }; 52 | 53 | Response.StatusCode = 200; 54 | Response.ContentType = "application/json"; 55 | Response.Write(JsonConvert.SerializeObject(response)); 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /ModniteServer.Api/Controllers/StorefrontController.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace ModniteServer.API.Controllers 4 | { 5 | public sealed class StorefrontController : Controller 6 | { 7 | /// 8 | /// Gets the keys needed to decrypt some assets. 9 | /// 10 | [Route("GET", "/fortnite/api/storefront/v2/keychain")] 11 | public void GetKeychain() 12 | { 13 | // NOTE: This API gives the encryption key needed to decrypt data for pakchunk1000+ 14 | 15 | var response = new[] 16 | { 17 | // [GUID]:[base64 encoded key]:[skin] 18 | 19 | // TODO: 0xdc434988002105581bae813d59b92358ae58a8bf2fcc9d2c14a85a75129c4239 CID_255_Athena_Commando_F_HalloweenBunny 20 | "D8FDE5644FE474C7C3D476AA18426FEB:orzs/Wp9XxE5vpybtd0tOxX6hrMyZZheFZusAw1c+6A=:BID_126_DarkBomber", 21 | "8F6ACF5D43BC4BC272D72EBC072BDB4F:rsT5K8O82gjB/BWAR7zl6cBstk0xxiu/E0AK/RQNUjE=:CID_246_Athena_Commando_F_Grave", 22 | "4A8216304A1A18CB9583BC8CFF99EE26:QF3nHCFt1vhELoU4q1VKTmpxnk20c2iAiBEBzlbzQAY=:CID_184_Athena_Commando_M_DurrburgerWorker", 23 | "FE0FA56F4B280D2F0CB2AB899C645F3E:hYi0DrAf6wtw7Zi+PlUi7/vIlIB3psBzEb5piGLEW6s=:CID_220_Athena_Commando_F_Clown", 24 | "D50ABA0F48BD66E4044616BDC40F4AD6:GNzmjA0ytPrD6J//HSbVF0qypflabpJ3guKTdX4ZStE=:BID_115_DieselpunkMale", 25 | "7FA4F2374FFE075000BC209360056A5A:nywIiZlIL8AIMkwCZfrYoAkpHM3zCwddhfszh++6ejI=:CID_223_Athena_Commando_M_Dieselpunk", 26 | "E45BD1CD4B6669367E57AFB5DC2B4478:EXfrtfslMES/Z2M/wWCEYeQoWzI1GTRaElXhaHBw8YM=:CID_229_Athena_Commando_F_DarkBomber" 27 | }; 28 | 29 | Response.StatusCode = 200; 30 | Response.ContentType = "application/json"; 31 | Response.Write(JsonConvert.SerializeObject(response)); 32 | } 33 | 34 | [Route("POST", "/fortnite/api/game/v2/profile/*/client/PurchaseCatalogEntry")] 35 | public void PurchaseItem() 36 | { 37 | string accountId = Request.Url.Segments[Request.Url.Segments.Length - 3].Trim('/'); 38 | Query.TryGetValue("profileId", out string profileId); 39 | Query.TryGetValue("rvn", out string rvn); 40 | 41 | var response = new 42 | { 43 | // TODO 44 | }; 45 | 46 | Response.StatusCode = 200; 47 | Response.ContentType = "application/json"; 48 | Response.Write(JsonConvert.SerializeObject(response)); 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /ModniteServer/Controls/LogStream.xaml.cs: -------------------------------------------------------------------------------- 1 | using Serilog.Events; 2 | using Serilog.Parsing; 3 | using System.Collections.ObjectModel; 4 | using System.Text; 5 | using System.Windows.Controls; 6 | using System.Windows.Input; 7 | 8 | namespace ModniteServer.Controls 9 | { 10 | public partial class LogStream : UserControl 11 | { 12 | public LogStream() 13 | { 14 | InitializeComponent(); 15 | 16 | AutoScroll = true; 17 | this.scrollViewer.ScrollChanged += ScrollViewer_ScrollChanged; 18 | this.scrollViewer.PreviewMouseWheel += ScrollViewer_PreviewMouseWheel; 19 | } 20 | 21 | public bool AutoScroll { get; set; } 22 | 23 | public async void AppendEvent(LogEvent logEvent) 24 | { 25 | await Dispatcher.InvokeAsync(() => 26 | { 27 | var message = new StringBuilder(); 28 | var properties = new ObservableCollection<(string Key, string Value)>(); 29 | 30 | foreach (var token in logEvent.MessageTemplate.Tokens) 31 | { 32 | switch (token) 33 | { 34 | case TextToken textToken: 35 | message.Append(textToken); 36 | break; 37 | 38 | case PropertyToken propToken: 39 | string propValue = logEvent.Properties[propToken.PropertyName].ToString(); 40 | properties.Add((propToken.PropertyName, propValue)); 41 | break; 42 | } 43 | } 44 | 45 | var entry = new LogStreamEntry 46 | { 47 | Text = message.ToString(), 48 | Properties = properties 49 | }; 50 | 51 | this.logStreamPanel.Children.Add(entry); 52 | 53 | if (AutoScroll) 54 | { 55 | this.scrollViewer.ScrollToEnd(); 56 | this.scrollViewer.ScrollToLeftEnd(); 57 | } 58 | }); 59 | } 60 | 61 | void ScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e) 62 | { 63 | AutoScroll = (this.scrollViewer.VerticalOffset == this.scrollViewer.ScrollableHeight); 64 | } 65 | 66 | void ScrollViewer_PreviewMouseWheel(object sender, MouseWheelEventArgs e) 67 | { 68 | this.scrollViewer.ScrollToVerticalOffset(this.scrollViewer.VerticalOffset - e.Delta); 69 | e.Handled = true; 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /ModniteServer.Services/Xmpp/XmppClient.cs: -------------------------------------------------------------------------------- 1 | using Serilog; 2 | using System.Net.Sockets; 3 | using System.Xml.Linq; 4 | 5 | namespace ModniteServer.Xmpp 6 | { 7 | internal sealed class XmppClient 8 | { 9 | public XmppClient(XmppServer owner, Socket socket) 10 | { 11 | Server = owner; 12 | Socket = socket; 13 | } 14 | 15 | public XmppServer Server { get; } 16 | 17 | public Socket Socket { get; } 18 | 19 | internal void HandleMessage(XElement element, out XElement response) 20 | { 21 | bool messageHandled = false; 22 | 23 | switch (element.Name.LocalName) 24 | { 25 | case "iq": 26 | { 27 | string id = element.Attribute("id").Value; 28 | string type = element.Attribute("type").Value; 29 | 30 | if (id == "_xmpp_auth1") 31 | { 32 | XNamespace authNs = "jabber:iq:auth"; 33 | var query = element.Element(authNs + "query"); 34 | 35 | string username = query.Element(authNs + "username").Value; 36 | string password = query.Element(authNs + "password").Value; 37 | string resource = query.Element(authNs + "resource").Value; 38 | 39 | // TODO: Validate login request 40 | Log.Information("[XMPP] Login requested for '" + username + "'"); 41 | 42 | var loginSuccessfulResponse = new XElement("iq"); 43 | loginSuccessfulResponse.Add(new XAttribute("type", "result"), new XAttribute("id", "_xmpp_auth1")); 44 | 45 | Server.SendXmppMessage(Socket, loginSuccessfulResponse); 46 | messageHandled = true; 47 | } 48 | } 49 | break; 50 | 51 | case "close": 52 | { 53 | var closeResponse = new XElement("close", new XAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-framing")); 54 | Server.SendXmppMessage(Socket, closeResponse); 55 | messageHandled = true; 56 | } 57 | break; 58 | 59 | case "presence": 60 | // todo 61 | break; 62 | } 63 | 64 | if (!messageHandled) 65 | { 66 | Log.Warning("[XMPP] Uhandled message received {Message}", element); 67 | } 68 | 69 | response = null; 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /ModniteServer/Commands/GiveItemCommand.cs: -------------------------------------------------------------------------------- 1 | using ModniteServer.API.Accounts; 2 | using Serilog; 3 | 4 | namespace ModniteServer.Commands 5 | { 6 | /// 7 | /// User command for giving an item to an account. 8 | /// 9 | public sealed class GiveItemCommand : IUserCommand 10 | { 11 | public string Description => "Gives an item to an account. For cosmetics, use profile 'athena'. For banners, use profile 'common_core'"; 12 | public string Args => " "; 13 | public string ExampleArgs => "player123 athena AthenaCharacter:CID_185_Athena_Commando_M_DurrburgerHero"; 14 | 15 | public void Handle(string[] args) 16 | { 17 | if (args.Length == 3) 18 | { 19 | string accountId = args[0]; 20 | 21 | if (!AccountManager.AccountExists(accountId)) 22 | { 23 | Log.Error($"Account '{accountId}' does not exist"); 24 | return; 25 | } 26 | 27 | if (!args[2].Contains(":")) 28 | { 29 | Log.Error($"Invalid item id"); 30 | return; 31 | } 32 | 33 | var account = AccountManager.GetAccount(accountId); 34 | 35 | if (args[1] == "athena") 36 | { 37 | if (account.AthenaItems.Contains(args[2])) 38 | { 39 | Log.Error($"Account '{accountId}' already has the item '{args[2]}' in profile '{args[1]}'"); 40 | } 41 | else 42 | { 43 | account.AthenaItems.Add(args[2]); 44 | Log.Information($"Gave '{args[2]}' to '{accountId}' in profile '{args[1]}'"); 45 | } 46 | } 47 | else if (args[1] == "common_core") 48 | { 49 | if (account.AthenaItems.Contains(args[2])) 50 | { 51 | Log.Error($"Account '{accountId}' already has the item '{args[2]}' in profile '{args[1]}'"); 52 | } 53 | else 54 | { 55 | account.CoreItems.Add(args[2]); 56 | Log.Information($"Gave '{args[2]}' to '{accountId}' in profile '{args[1]}'"); 57 | } 58 | } 59 | else 60 | { 61 | Log.Error($"Invalid profile '{args[1]}'"); 62 | } 63 | 64 | AccountManager.SaveAccounts(); 65 | } 66 | else 67 | { 68 | Log.Error("Invalid arguments"); 69 | } 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Modnite Server 2 | Modnite Server is a private server for Fortnite Battle Royale. Since Fortnite Battle Royale is an online PvP game, there is no way to mod the game without disrupting other players. With Modnite Server, players can host their own server and be free to mod the game without ruining the game for everyone else. 3 | 4 | We ❤️ Fortnite and hope this creates an exciting new experience for the game. 5 | 6 | ![Server screenshot](/docs/screenshot.png) 7 | ![Lobby screenshot](/docs/Version6_10.png) 8 | 9 | # Milestones 10 | ✔️ Enable editing of game files and disable TLS 11 | 12 | ✔️ Reach the lobby 13 | 14 | Done by polaris era and rif 15 | 16 | ➖ Very good multiplayer support 17 | 18 | # Releases 19 | Go to [Releases](https://github.com/ModniteNet/ModniteServer/releases) for compiled binaries. 20 | 21 | # FAQs 22 | ### What can I do with a private server? 23 | At this time, Modnite Server lets you: 24 | * Get to the lobby 25 | * Equip anything you want 26 | 27 | This is a very early preview. There's not much you can do at this time. 28 | 29 | ### How do I get Fortnite to connect to my server? 30 | [Follow this guide](https://www.modnite.net/guides/easy-way-connect-to-a-private-server-using-modnite-patcher-r8/). 31 | 32 | ### How do I create accounts? 33 | By default, Modnite Server will automatically create new accounts. Simply login with the email `username@modnite.net`. If the username does not exist, a new account will be created. 34 | 35 | For example, logging in as `wumbo@modnite.net` will create a new account with the display name set to `wumbo`. 36 | ![Login example](/docs/login.PNG) 37 | 38 | Alternatively, you may use the `create` command. For example, `create player123 hunter2`. 39 | 40 | ### Isn't Epic Games testing a custom matchmaking system? 41 | Sure, but that's not really the point of this project. We want to enable players to mod the game without interfering with other players. Given the PvP nature of Battle Royale, it's unlikely Epic Games would allow modding at all. 42 | 43 | ### How do I change the item shop? 44 | To mitigate clickbait and scams, access to the item shop and V-Bucks store is restricted. Modnite Server maintains a perpetual item shop consisting of only default skins with all prices set to 0 V-Bucks. We may eventually open-source the item shop feature, but that won't happen any time soon. 45 | 46 | ### Will you ever add support for Save the World? 47 | Save the World is off limits until it becomes free. 48 | 49 | ### How can I contribute? 50 | Visit the [Reverse Engineering forum on Modnite.net](https://www.modnite.net/forum/17-reverse-engineering/) to participate. Set `LogHttp` and `Log404` to `true` in `config.json` to enable logging of HTTP requests which will help you implement controllers and routes for the missing features. Pull requests are also welcome. 51 | -------------------------------------------------------------------------------- /ModniteServer.Api/Controllers/CloudStorageController.cs: -------------------------------------------------------------------------------- 1 | using Serilog; 2 | using System.IO; 3 | using System.Linq; 4 | 5 | namespace ModniteServer.API.Controllers 6 | { 7 | public sealed class CloudStorageController : Controller 8 | { 9 | /// 10 | /// Gets the list of system files to download from the cloud storage service. 11 | /// 12 | [Route("GET", "/fortnite/api/cloudstorage/system")] 13 | public void GetSystemFileList() 14 | { 15 | if (!Authorize()) { } 16 | 17 | Response.StatusCode = 200; 18 | Response.ContentType = "application/json"; 19 | Response.Write("[]"); 20 | 21 | /* 22 | * [ 23 | * { 24 | * "uniqueFileName": "", 25 | * "filename": "DefaultRuntimeOptions.ini", 26 | * "hash": "" 27 | * "hash256": "" 28 | * "length": 29 | * "contentType": "application/octet-stream" 30 | * "uploaded": "" 31 | * "storageType": "S3" 32 | * "doNotCache": bool 33 | * ] 34 | */ 35 | } 36 | 37 | [Route("GET", "/fortnite/api/cloudstorage/user/*")] 38 | public void GetUserFileList() 39 | { 40 | // TODO: Implement cloud storage system 41 | if (!Authorize()) { } 42 | 43 | Response.StatusCode = 200; 44 | Response.ContentType = "application/json"; 45 | Response.Write("[]"); 46 | } 47 | 48 | [Route("GET", "/fortnite/api/cloudstorage/user/*/*")] 49 | public void GetUserFile() 50 | { 51 | if (!Authorize()) { } 52 | 53 | string accountId = Request.Url.Segments.Reverse().Skip(1).Take(1).Single(); 54 | string file = Request.Url.Segments.Last(); 55 | 56 | if (file.ToLowerInvariant() == "clientsettings.sav") 57 | { 58 | // For now, we're giving the default settings. Eventually, we'll be storing user settings. 59 | Response.StatusCode = 200; 60 | Response.ContentType = "application/octet-stream"; 61 | Response.Write(File.ReadAllBytes("Assets/ClientSettings.Sav")); 62 | } 63 | else 64 | { 65 | Response.StatusCode = 404; 66 | } 67 | 68 | Log.Information($"{accountId} downloaded {file} {{AccountId}}{{File}}", accountId, file); 69 | } 70 | 71 | // todo 72 | [Route("PUT", "/fortnite/api/cloudstorage/user/*/*")] 73 | public void SaveUserFile() 74 | { 75 | if (!Authorize()) { } 76 | 77 | string accountId = Request.Url.Segments.Reverse().Skip(1).Take(1).Single(); 78 | string file = Request.Url.Segments.Last(); 79 | 80 | Response.StatusCode = 204; 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /ModniteServer.Services/Xmpp/XmppServer.cs: -------------------------------------------------------------------------------- 1 | using ModniteServer.Websockets; 2 | using Serilog; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Net; 6 | using System.Net.Sockets; 7 | using System.Text; 8 | using System.Xml.Linq; 9 | 10 | namespace ModniteServer.Xmpp 11 | { 12 | public sealed class XmppServer : IDisposable 13 | { 14 | private Dictionary _clients; 15 | private bool _isDisposed; 16 | 17 | private readonly WebsocketServer _server; 18 | 19 | public XmppServer(ushort port) 20 | { 21 | Port = port; 22 | 23 | _server = new WebsocketServer(Port); 24 | _server.MessageReceived += OnMessageReceived; 25 | 26 | _clients = new Dictionary(); 27 | } 28 | 29 | public ILogger Logger 30 | { 31 | get { return Log.Logger; } 32 | set { Log.Logger = value; } 33 | } 34 | 35 | public ushort Port { get; } 36 | 37 | public void Start() 38 | { 39 | _server.Start(); 40 | } 41 | 42 | public void Dispose() 43 | { 44 | if (!_isDisposed) 45 | { 46 | _server.Dispose(); 47 | _isDisposed = true; 48 | } 49 | } 50 | 51 | internal void SendXmppMessage(Socket socket, XElement message) 52 | { 53 | Log.Information("[Xmpp] Sent XMPP message {Message}", message); 54 | _server.SendMessage(socket, MessageType.Text, Encoding.UTF8.GetBytes(message.ToString())); 55 | } 56 | 57 | private void OnMessageReceived(object sender, WebsocketMessageReceivedEventArgs e) 58 | { 59 | var element = XElement.Parse(e.Message.TextContent); 60 | if (element.Name.LocalName == "open") 61 | { 62 | // XMPP handshake 63 | XNamespace xmlns = "urn:ietf:params:xml:ns:xmpp-framing"; 64 | XNamespace xml = "xml"; 65 | var response = new XElement( 66 | xmlns + "open", 67 | new XAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-framing"), 68 | new XAttribute("from", ""), 69 | new XAttribute("id", "xmpp_id_yolo"), 70 | new XAttribute(xml + "lang", "en"), 71 | new XAttribute("version", "1.0") 72 | ); 73 | SendXmppMessage(e.Socket, response); 74 | 75 | _clients.Add(e.Socket.RemoteEndPoint, new XmppClient(this, e.Socket)); 76 | Log.Information("New XMPP client {Client}", e.Socket.RemoteEndPoint); 77 | } 78 | else 79 | { 80 | _clients[e.Socket.RemoteEndPoint].HandleMessage(element, out XElement response); 81 | if (response != null) 82 | { 83 | SendXmppMessage(e.Socket, response); 84 | } 85 | } 86 | } 87 | } 88 | } -------------------------------------------------------------------------------- /ModniteServer/Properties/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace ModniteServer.Properties 12 | { 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resources 26 | { 27 | 28 | private static global::System.Resources.ResourceManager resourceMan; 29 | 30 | private static global::System.Globalization.CultureInfo resourceCulture; 31 | 32 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 33 | internal Resources() 34 | { 35 | } 36 | 37 | /// 38 | /// Returns the cached ResourceManager instance used by this class. 39 | /// 40 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 41 | internal static global::System.Resources.ResourceManager ResourceManager 42 | { 43 | get 44 | { 45 | if ((resourceMan == null)) 46 | { 47 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("ModniteServer.Properties.Resources", typeof(Resources).Assembly); 48 | resourceMan = temp; 49 | } 50 | return resourceMan; 51 | } 52 | } 53 | 54 | /// 55 | /// Overrides the current thread's CurrentUICulture property for all 56 | /// resource lookups using this strongly typed resource class. 57 | /// 58 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 59 | internal static global::System.Globalization.CultureInfo Culture 60 | { 61 | get 62 | { 63 | return resourceCulture; 64 | } 65 | set 66 | { 67 | resourceCulture = value; 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /ModniteServer/Views/MainWindow.xaml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /ModniteServer/ObservableObject.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel; 4 | using System.Linq.Expressions; 5 | using System.Runtime.CompilerServices; 6 | 7 | namespace ModniteServer 8 | { 9 | /// 10 | /// Provides a base implementation of allowing properties 11 | /// of derived classes to be observed for changes. 12 | /// 13 | public abstract class ObservableObject 14 | : INotifyPropertyChanged 15 | { 16 | /// 17 | /// Occurs when a property value changes. 18 | /// 19 | public event PropertyChangedEventHandler PropertyChanged = delegate { }; 20 | 21 | /// 22 | /// Assigns the specified to the specified 23 | /// reference, raising the event if the value has been changed. 24 | /// 25 | /// The type of the value. 26 | /// The reference to a field. 27 | /// The value to assign to the field. 28 | /// 29 | /// If false, always raise the event regardless of whether 30 | /// the value remains the same before and after this method's invocation. 31 | /// 32 | /// 33 | /// The name of the property. Leave as default to use the member name of the caller of this method. 34 | /// 35 | /// 36 | /// A boolean value indicating whether the event was raised. 37 | /// 38 | protected bool SetValue(ref T field, T value, bool useEqualityCheck = false, [CallerMemberName] string propertyName = "") 39 | { 40 | if (useEqualityCheck && EqualityComparer.Default.Equals(field, value)) 41 | return false; 42 | 43 | field = value; 44 | OnPropertyChanged(propertyName); 45 | 46 | return true; 47 | } 48 | 49 | /// 50 | /// Raises the event. 51 | /// 52 | /// 53 | /// The name of the property. Leave as default to use the member name of the caller of this method. 54 | /// 55 | protected void OnPropertyChanged([CallerMemberName] string propertyName = "") 56 | { 57 | PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); 58 | } 59 | 60 | /// 61 | /// Raises the event for another property. 62 | /// 63 | /// 64 | /// 65 | protected void OnPropertyChanged(Expression> propertyExpression) 66 | { 67 | var body = propertyExpression.Body as MemberExpression; 68 | var expression = body.Expression as ConstantExpression; 69 | PropertyChanged(expression.Value, new PropertyChangedEventArgs(body.Member.Name)); 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /ModniteServer.Api/Controllers/AccessController.cs: -------------------------------------------------------------------------------- 1 | using ModniteServer.API.Accounts; 2 | using Newtonsoft.Json; 3 | using Serilog; 4 | using System; 5 | using System.Linq; 6 | 7 | namespace ModniteServer.API.Controllers 8 | { 9 | /// 10 | /// Handles requests for access including version, platform, and account checks. 11 | /// 12 | public sealed class AccessController : Controller 13 | { 14 | /// 15 | /// Checks whether the given version and platform needs an update. 16 | /// 17 | [Route("GET", "/fortnite/api/v2/versioncheck/*")] 18 | public void CheckVersion() 19 | { 20 | if (!Authorize()) return; 21 | 22 | string platform = Request.Url.Segments.Last(); 23 | 24 | if (Query.TryGetValue("version", out string versionInfo)) 25 | { 26 | // Parse version and check if it meets the minimum version requirements. 27 | versionInfo = versionInfo.Substring("++Fortnite+Release-".Length); 28 | versionInfo = versionInfo.Replace("-" + platform, ""); 29 | int.TryParse(versionInfo.Substring(0, versionInfo.IndexOf('.')), out int major); 30 | int.TryParse(versionInfo.Substring(versionInfo.IndexOf('.') + 1, versionInfo.IndexOf('-') - 2), out int minor); 31 | int.TryParse(versionInfo.Substring(versionInfo.LastIndexOf('-') + 1), out int build); 32 | var version = new Version(major, minor, build); 33 | 34 | // HACK: Temporary workaround to get matchmaking access in v6.10 35 | if (version.Major == 6 && version.Minor == 10) 36 | { 37 | var response = new 38 | { 39 | type = "NO_UPDATE" // NO_UPDATE | NOT_ENABLED | SOFT_UPDATE | HARD_UPDATE | APP_REDIRECT 40 | }; 41 | 42 | Response.StatusCode = 200; 43 | Response.ContentType = "application/json"; 44 | Response.Write(JsonConvert.SerializeObject(response)); 45 | } 46 | 47 | // Sending NO_UPDATE will cause the "Game was not launched correctly" error, 48 | // so we're sending the client nothing. 49 | Response.StatusCode = 204; 50 | 51 | return; 52 | } 53 | 54 | Response.StatusCode = 403; 55 | } 56 | 57 | /// 58 | /// Checks whether the account has the privilege of playing on this platform. 59 | /// 60 | [Route("POST", "/fortnite/api/game/v2/tryPlayOnPlatform/account/*")] 61 | public void TryPlayOnPlatform() 62 | { 63 | if (!Authorize()) return; 64 | 65 | string accountId = Request.Url.Segments.Last(); 66 | Query.TryGetValue("platform", out string platform); 67 | 68 | if (!AccountManager.AccountExists(accountId)) 69 | { 70 | Response.StatusCode = 404; 71 | return; 72 | } 73 | 74 | Response.StatusCode = 200; 75 | Response.ContentType = "text/plain"; 76 | Response.Write("true"); 77 | } 78 | 79 | /// 80 | /// Grants access to the game for the given account. 81 | /// 82 | [Route("POST", "/fortnite/api/game/v2/grant_access/*")] 83 | public void GrantAccess() 84 | { 85 | if (!Authorize()) return; 86 | 87 | string accountId = Request.Url.Segments.Last(); 88 | 89 | if (!AccountManager.AccountExists(accountId)) 90 | { 91 | Response.StatusCode = 404; 92 | return; 93 | } 94 | 95 | Response.StatusCode = 204; 96 | } 97 | } 98 | } -------------------------------------------------------------------------------- /ModniteServer.Api/Controllers/AccountController.cs: -------------------------------------------------------------------------------- 1 | using ModniteServer.API.Accounts; 2 | using Newtonsoft.Json; 3 | using Serilog; 4 | using System.Linq; 5 | 6 | namespace ModniteServer.API.Controllers 7 | { 8 | /// 9 | /// Handles requests for retrieving info on accounts. 10 | /// 11 | public sealed class AccountController : Controller 12 | { 13 | /// 14 | /// Retrieves basic info for an account. 15 | /// 16 | [Route("GET", "/account/api/public/account/*")] 17 | public void GetAccountInfo() 18 | { 19 | if (!Authorize()) return; 20 | 21 | string accountId = Request.Url.Segments.Last(); 22 | 23 | if (!AccountManager.AccountExists(accountId)) 24 | { 25 | Response.StatusCode = 404; 26 | return; 27 | } 28 | 29 | var account = AccountManager.GetAccount(accountId); 30 | 31 | var response = new 32 | { 33 | id = account.AccountId, 34 | displayname = account.DisplayName, 35 | name = account.FirstName, 36 | email = account.Email, 37 | failedLoginAttempts = account.FailedLoginAttempts, 38 | lastLogin = account.LastLogin.ToDateTimeString(), 39 | numberOfDisplayNameChanges = account.DisplayNameHistory?.Count ?? 0, 40 | ageGroup = "UNKNOWN", 41 | headless = false, 42 | country = account.Country, 43 | preferredLanguage = account.PreferredLanguage, 44 | 45 | // We're not supporting 2FA for Modnite. 46 | tfaEnabled = false 47 | }; 48 | 49 | Log.Information($"Account info retrieved for {accountId} {{AccountInfo}}", response); 50 | 51 | Response.StatusCode = 200; 52 | Response.ContentType = "application/json"; 53 | Response.Write(JsonConvert.SerializeObject(response)); 54 | } 55 | 56 | /// 57 | /// Gets the linked account info for an account. Since we don't provide the capability of 58 | /// signing into accounts using Facebook, Google, or whatever, we're just going to provide 59 | /// the same info as . 60 | /// 61 | [Route("GET", "/account/api/public/account/*/externalAuths")] 62 | public void GetExternalAuthInfo() 63 | { 64 | if (!Authorize()) return; 65 | 66 | string accountId = Request.Url.Segments[Request.Url.Segments.Length - 2]; 67 | 68 | if (!AccountManager.AccountExists(accountId)) 69 | { 70 | Response.StatusCode = 404; 71 | return; 72 | } 73 | 74 | var account = AccountManager.GetAccount(accountId); 75 | 76 | var response = new 77 | { 78 | url = "", 79 | id = account.AccountId, 80 | externalAuthId = account.AccountId, 81 | externalDisplayName = account.DisplayName, 82 | externalId = account.AccountId, 83 | externalauths = "epic", 84 | users = new [] 85 | { 86 | new 87 | { 88 | externalAuthId = account.AccountId, 89 | externalDisplayName = account.DisplayName, 90 | externalId = account.AccountId 91 | } 92 | } 93 | }; 94 | 95 | Response.StatusCode = 200; 96 | Response.ContentType = "application/json"; 97 | Response.Write(JsonConvert.SerializeObject(response)); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /ModniteServer.Api/Controllers/FriendsController.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace ModniteServer.API.Controllers 6 | { 7 | public sealed class FriendsController : Controller 8 | { 9 | [Route("GET", "/friends/api/public/friends/*")] 10 | public void GetFriends() 11 | { 12 | string accountId = Request.Url.Segments.Last(); 13 | 14 | var response = new List(); 15 | 16 | Response.StatusCode = 200; 17 | Response.ContentType = "application/json"; 18 | Response.Write(JsonConvert.SerializeObject(response)); 19 | } 20 | 21 | [Route("GET", "/friends/api/public/blocklist/*")] 22 | public void GetBlocklist() 23 | { 24 | string accountId = Request.Url.Segments.Last(); 25 | 26 | var response = new 27 | { 28 | blockedUsers = new List() 29 | }; 30 | 31 | Response.StatusCode = 200; 32 | Response.ContentType = "application/json"; 33 | Response.Write(JsonConvert.SerializeObject(response)); 34 | } 35 | 36 | [Route("GET", "/friends/api/public/list/fortnite/*/recentPlayers")] 37 | public void GetRecentPlayers() 38 | { 39 | string accountId = Request.Url.Segments[Request.Url.Segments.Length - 2].Replace("/", ""); 40 | 41 | var response = new List(); 42 | 43 | Response.StatusCode = 200; 44 | Response.ContentType = "application/json"; 45 | Response.Write(JsonConvert.SerializeObject(response)); 46 | } 47 | 48 | [Route("GET", "/friends/api/v1/*/settings")] 49 | public void GetSettings() 50 | { 51 | string accountId = Request.Url.Segments[Request.Url.Segments.Length - 2].Replace("/", ""); 52 | 53 | var response = new 54 | { 55 | acceptInvites = "public" 56 | }; 57 | 58 | Response.StatusCode = 200; 59 | Response.ContentType = "application/json"; 60 | Response.Write(JsonConvert.SerializeObject(response)); 61 | } 62 | 63 | [Route("GET", "/account/api/public/account/displayName/*")] 64 | public void FindFriendByDisplayName() 65 | { 66 | string displayName = Request.Url.Segments.Last(); 67 | string type = Request.Url.Segments[Request.Url.Segments.Length - 2].Replace("/", ""); 68 | 69 | var response = new 70 | { 71 | errorCode = "errors.com.epicgames.account.account_not_found", 72 | errorMessage = "Sorry, we couldn't find an account for " + displayName, 73 | messageVars = new List { displayName }, 74 | numericErrorCode = 18007, 75 | originatingService = "com.epicgames.account.public", 76 | intent = "prod" 77 | }; 78 | 79 | Response.StatusCode = 404; 80 | Response.ContentType = "application/json"; 81 | Response.Write(JsonConvert.SerializeObject(response)); 82 | } 83 | 84 | [Route("GET", "/account/api/public/account/email/*")] 85 | public void FindFriendByEmail() 86 | { 87 | string email = Request.Url.Segments.Last(); 88 | 89 | var response = new 90 | { 91 | errorCode = "errors.com.epicgames.account.account_not_found", 92 | errorMessage = "Sorry, we couldn't find an account for " + email, 93 | messageVars = new List { email }, 94 | numericErrorCode = 18007, 95 | originatingService = "com.epicgames.account.public", 96 | intent = "prod" 97 | }; 98 | 99 | Response.StatusCode = 404; 100 | Response.ContentType = "application/json"; 101 | Response.Write(JsonConvert.SerializeObject(response)); 102 | } 103 | } 104 | } -------------------------------------------------------------------------------- /ModniteServer.Api/Controllers/CalendarController.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.Collections.Generic; 4 | 5 | namespace ModniteServer.API.Controllers 6 | { 7 | public class CalendarController : Controller 8 | { 9 | [Route("GET", "/fortnite/api/calendar/v1/timeline")] 10 | public void GetTimeline() 11 | { 12 | if (!Authorize()) { } 13 | 14 | // Events never expire in Modnite Server. The start/end dates are always relative to today. 15 | var clientEvents = new List(); 16 | foreach (string evt in ApiConfig.Current.ClientEvents) 17 | { 18 | clientEvents.Add(new 19 | { 20 | eventType = "EventFlag." + evt, 21 | activeUntil = DateTime.Now.Date.AddDays(7).ToDateTimeString(), 22 | activeSince = DateTime.Now.Date.AddDays(-1).ToDateTimeString() 23 | }); 24 | } 25 | 26 | var response = new 27 | { 28 | channels = new Dictionary 29 | { 30 | ["client-matchmaking"] = new 31 | { 32 | // This section is used to disable game modes in certain regions. 33 | states = new [] { 34 | new 35 | { 36 | validFrom = DateTime.Now.Date.AddDays(-1).ToDateTimeString(), 37 | activeEvents = new string[0], 38 | state = new 39 | { 40 | region = new { } 41 | } 42 | } 43 | }, 44 | cacheExpire = DateTime.Now.Date.AddDays(7).ToDateTimeString() 45 | }, 46 | ["client-events"] = new 47 | { 48 | // You can set event flags in config.json. Visit https://modnite.net for a list of event flags. 49 | // Event flags control stuff like lobby theme, battle bus skin, etc. 50 | states = new[] 51 | { 52 | new 53 | { 54 | validFrom = DateTime.Now.AddDays(-1).ToDateTimeString(), 55 | activeEvents = clientEvents, 56 | state = new 57 | { 58 | activeStorefronts = new string[0], 59 | eventNamedWeights = new { }, 60 | activeEvents = new string[0], 61 | seasonNumber = 0, 62 | seasonTemplateId = "", 63 | matchXpBonusPoints = 0, // Bonus XP Event 64 | seasonBegin = DateTime.Now.Date.AddDays(-30).ToDateTimeString(), 65 | seasonEnd = DateTime.Now.Date.AddDays(30).ToDateTimeString(), 66 | seasonDisplayedEnd = DateTime.Now.Date.AddDays(30).ToDateTimeString(), 67 | weeklyStoreEnd = DateTime.Now.Date.AddDays(7).ToDateTimeString(), 68 | stwEventStoreEnd = DateTime.Now.Date.AddDays(7).ToDateTimeString(), 69 | stwWeeklyStoreEnd = DateTime.Now.Date.AddDays(7).ToDateTimeString() 70 | } 71 | } 72 | }, 73 | cacheExpire = DateTime.Now.Date.AddDays(7).ToDateTimeString() 74 | } 75 | }, 76 | eventsTimeOffsetHrs = 0.0, 77 | currentTime = DateTime.Now.ToDateTimeString() 78 | }; 79 | 80 | Response.StatusCode = 200; 81 | Response.ContentType = "application/json"; 82 | Response.Write(JsonConvert.SerializeObject(response)); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /ModniteServer/ViewModels/MainViewModel.cs: -------------------------------------------------------------------------------- 1 | using ModniteServer.API; 2 | using ModniteServer.Matchmaker; 3 | using ModniteServer.Xmpp; 4 | using Serilog; 5 | using System; 6 | using System.Net; 7 | using System.Net.Http; 8 | using System.Threading.Tasks; 9 | 10 | namespace ModniteServer.ViewModels 11 | { 12 | public sealed class MainViewModel : ObservableObject 13 | { 14 | public const int CurrentVersion = 2; 15 | 16 | private string _title; 17 | private string _command; 18 | 19 | private ApiServer _apiServer; 20 | private XmppServer _xmppServer; 21 | private MatchmakerServer _matchmakerServer; 22 | 23 | public MainViewModel() 24 | { 25 | Title = "Modnite Server"; 26 | 27 | Log.Information("Modnite Server v1.1-Alpha"); 28 | Log.Information("Made with ❤️ by the Modnite Contributors"); 29 | 30 | StartServices(); 31 | 32 | Log.Information("Type 'commands' to see a list of available commands"); 33 | 34 | Task.Run(CheckForUpdatesAsync); 35 | } 36 | 37 | /// 38 | /// Gets or sets the title of this app. 39 | /// 40 | public string Title 41 | { 42 | get { return _title; } 43 | set { SetValue(ref _title, value); } 44 | } 45 | 46 | /// 47 | /// Gets or sets the current command. 48 | /// 49 | public string Command 50 | { 51 | get { return _command; } 52 | set { SetValue(ref _command, value); } 53 | } 54 | 55 | /// 56 | /// Invokes the command stored in . 57 | /// 58 | public void InvokeCommand() 59 | { 60 | CommandManager.InvokeCommand(Command); 61 | Command = ""; 62 | } 63 | 64 | /// 65 | /// Compares the current version with the newest version to see if an update is available. 66 | /// 67 | private async Task CheckForUpdatesAsync() 68 | { 69 | using (var client = new HttpClient()) 70 | { 71 | try 72 | { 73 | int latestVersion = Convert.ToInt32(await client.GetStringAsync("https://modniteimages.azurewebsites.net/update.txt")); 74 | if (CurrentVersion < latestVersion) 75 | { 76 | Log.Information("NEW VERSION AVAILABLE! Type 'update' to get the latest version."); 77 | } 78 | else 79 | { 80 | Log.Information("Modnite Server is up to date"); 81 | } 82 | } 83 | catch 84 | { 85 | Log.Error("Could not check for updates"); 86 | } 87 | } 88 | } 89 | 90 | /// 91 | /// Starts services. 92 | /// 93 | private void StartServices() 94 | { 95 | var config = ApiConfig.Current; 96 | ApiConfig.Current.Logger = Log.Logger; 97 | 98 | // Start the API server. 99 | _apiServer = new ApiServer(ApiConfig.Current.Port) 100 | { 101 | Logger = Log.Logger, 102 | LogHttpRequests = ApiConfig.Current.LogHttpRequests, 103 | Log404 = ApiConfig.Current.Log404, 104 | AlternativeRoute = "/fortnite-gameapi" 105 | }; 106 | try 107 | { 108 | _apiServer.Start(); 109 | Log.Information("API server started on port " + _apiServer.Port); 110 | } 111 | catch (HttpListenerException) when (_apiServer.Port == 80) 112 | { 113 | Log.Fatal("You must run this program as an administrator to use port 80!"); 114 | return; 115 | } 116 | 117 | // Start the XMPP service. 118 | _xmppServer = new XmppServer(ApiConfig.Current.XmppPort) 119 | { 120 | Logger = Log.Logger 121 | }; 122 | _xmppServer.Start(); 123 | Log.Information("XMPP server started on port " + _xmppServer.Port); 124 | 125 | // Start the matchmaker server. 126 | _matchmakerServer = new MatchmakerServer(ApiConfig.Current.MatchmakerPort) 127 | { 128 | Logger = Log.Logger 129 | }; 130 | _matchmakerServer.Start(); 131 | Log.Information("Matchmaker server started on port " + _matchmakerServer.Port); 132 | 133 | // TODO: Start the game server. 134 | } 135 | } 136 | } -------------------------------------------------------------------------------- /ModniteServer.Api/Accounts/AccountManager.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Serilog; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Reflection; 7 | 8 | namespace ModniteServer.API.Accounts 9 | { 10 | /// 11 | /// Provides methods for accessing and creating player accounts. 12 | /// 13 | public static class AccountManager 14 | { 15 | public const string AccountsFolder = @"\Accounts\"; 16 | public static readonly string AccountsFolderPath; 17 | 18 | private static ISet _retrievedAccounts; 19 | 20 | /// 21 | /// Initializes the account manager. 22 | /// 23 | static AccountManager() 24 | { 25 | string location = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); 26 | AccountsFolderPath = location + AccountsFolder; 27 | 28 | if (!Directory.Exists(AccountsFolderPath)) 29 | { 30 | Directory.CreateDirectory(AccountsFolderPath); 31 | } 32 | 33 | _retrievedAccounts = new HashSet(); 34 | } 35 | 36 | /// 37 | /// Saves all loaded accounts. This should be called before the program closes. 38 | /// 39 | public static void SaveAccounts() 40 | { 41 | foreach (var account in _retrievedAccounts) 42 | { 43 | string path = Path.Combine(AccountsFolderPath, account.AccountId) + ".json"; 44 | File.WriteAllText(path, JsonConvert.SerializeObject(account, Formatting.Indented)); 45 | } 46 | } 47 | 48 | /// 49 | /// Checks whether the given account exists. 50 | /// 51 | /// The email or id associated with the account. 52 | /// true if the account exists; Otherwise, false. 53 | public static bool AccountExists(string accountId) 54 | { 55 | if (string.IsNullOrWhiteSpace(accountId)) 56 | return false; 57 | 58 | if (accountId.Contains("@") || accountId.Contains("%40")) 59 | { 60 | accountId = accountId.Replace("%40", "@"); 61 | accountId = accountId.Substring(0, accountId.IndexOf('@')); 62 | } 63 | 64 | string path = Path.Combine(AccountsFolderPath, accountId) + ".json"; 65 | return File.Exists(path); 66 | } 67 | 68 | /// 69 | /// Gets the deserialized account data for the given user. 70 | /// 71 | /// The email or id associated with the account. 72 | /// The deserialized account data. 73 | public static Account GetAccount(string accountId) 74 | { 75 | if (string.IsNullOrWhiteSpace(accountId)) 76 | return null; 77 | 78 | if (accountId.Contains("@") || accountId.Contains("%40")) 79 | { 80 | accountId = accountId.Replace("%40", "@"); 81 | accountId = accountId.Substring(0, accountId.IndexOf('@')); 82 | } 83 | 84 | foreach (var loadedAccount in _retrievedAccounts) 85 | { 86 | if (loadedAccount.AccountId == accountId) 87 | return loadedAccount; 88 | } 89 | 90 | string path = Path.Combine(AccountsFolderPath, accountId) + ".json"; 91 | var account = JsonConvert.DeserializeObject(File.ReadAllText(path)); 92 | 93 | _retrievedAccounts.Add(account); 94 | return account; 95 | } 96 | 97 | /// 98 | /// Creates a new account with default values with the given credentials. 99 | /// 100 | /// The email to associate with this account. 101 | /// The hashed password for this account. 102 | /// The deserialized account data for the newly created account. 103 | public static Account CreateAccount(string email, string passwordHash) 104 | { 105 | if (email == null) throw new ArgumentNullException(nameof(email)); 106 | if (passwordHash == null) throw new ArgumentNullException(nameof(passwordHash)); 107 | 108 | email = email.Replace("%40", "@"); 109 | string username = email.Substring(0, email.IndexOf('@')); 110 | 111 | var account = new Account 112 | { 113 | Email = email, 114 | PasswordHash = passwordHash, 115 | DisplayName = username, 116 | AccountId = username, 117 | LastLogin = DateTime.UtcNow, 118 | AthenaItems = new HashSet(ApiConfig.Current.DefaultAthenaItems), 119 | CoreItems = new HashSet(ApiConfig.Current.DefaultCoreItems), 120 | EquippedItems = new Dictionary(ApiConfig.Current.EquippedItems) 121 | }; 122 | 123 | string path = Path.Combine(AccountsFolderPath, username) + ".json"; 124 | File.WriteAllText(path, JsonConvert.SerializeObject(account, Formatting.Indented)); 125 | 126 | Log.Information($"Created new account {account.DisplayName} {{Email}}{{DisplayName}}", email, account.DisplayName); 127 | 128 | _retrievedAccounts.Add(account); 129 | return account; 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /ModniteServer.Api/Controllers/MatchmakingController.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Serilog; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | 8 | namespace ModniteServer.API.Controllers 9 | { 10 | public sealed class MatchmakingController : Controller 11 | { 12 | /// 13 | /// Finds the available sessions for the player to rejoin. 14 | /// 15 | [Route("GET", "/fortnite/api/matchmaking/session/findPlayer/*")] 16 | public void FindPlayer() 17 | { 18 | var response = new List(); 19 | 20 | Response.StatusCode = 200; 21 | Response.ContentType = "application/json"; 22 | Response.Write(JsonConvert.SerializeObject(response)); 23 | } 24 | 25 | /// 26 | /// Gets a matchmaking ticket. 27 | /// 28 | [Route("GET", "/fortnite/api/game/v2/matchmakingservice/ticket/player/*")] 29 | public void GetTicket() 30 | { 31 | string accountId = Request.Url.Segments.Last(); 32 | 33 | Query.TryGetValue("bucketId", out string bucketId); 34 | Query.TryGetValue("player.subregions", out string playerSubregions); 35 | Query.TryGetValue("player.platform", out string playerPlatform); 36 | 37 | /* Query fields: 38 | * partyPlayerIds 39 | * bucketId 40 | * player.platform 41 | * player.subregions 42 | * player.option.custom_match_option.CreativeMutator_HealthAndShield:StartingShield 43 | * player.option.custom_match_option.CreativeMutator_HealthAndShield:StartingHealth 44 | * player.option.custom_match_option.CreativeMutator_ItemDropOverride:DropAllItemsOverride 45 | * player.option.custom_match_option.CreativeMutator_FallDamage:FallDamageMultiplier 46 | * player.option.custom_match_option.CreativeMutator_Gravity:GravityOverride 47 | * player.option.custom_match_option.CreativeMutator_TimeOfDay:TimeOfDayOverride 48 | * player.option.custom_match_option.CreativeMutator_NamePlates:DisplayMode 49 | * party.WIN 50 | * input.KBM 51 | */ 52 | 53 | var payload = new 54 | { 55 | playerId = accountId, 56 | partyPlayerIds = new [] 57 | { 58 | accountId 59 | }, 60 | bucketId = bucketId + ":PC:public:1", 61 | attributes = new Dictionary 62 | { 63 | ["player.subregions"] = playerSubregions, 64 | ["player.hasMultipleInputTypes"] = true, 65 | ["player.platform"] = playerPlatform, 66 | ["player.preferredSubregion"] = playerSubregions.Split(',')[0] 67 | }, 68 | expireAt = DateTime.Now.AddMinutes(30).ToDateTimeString(), 69 | nonce = "this_is_a_nonce" 70 | }; 71 | 72 | var response = new 73 | { 74 | serviceUrl = "ws://127.0.0.1:60103", 75 | ticketType = "mms-player", 76 | playload = Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(payload))), 77 | signature = "signature" // base64 encoded (sha256?) 78 | }; 79 | 80 | Response.StatusCode = 200; 81 | Response.ContentType = "application/json"; 82 | Response.Write(JsonConvert.SerializeObject(response)); 83 | } 84 | 85 | [Route("GET", "/fortnite/api/matchmaking/session/*")] 86 | public void GetMatchmakingSession() 87 | { 88 | string sessionId = Request.Url.Segments.Last(); 89 | 90 | Log.Information($"Queried matchmaking session '{sessionId}'"); 91 | 92 | // This shows a "Session has expired" error. Not sure why yet... 93 | var response = new 94 | { 95 | ownerId = "owner_id", 96 | ownerName = "owner_name", 97 | serverName = "server_name", 98 | openPrivatePlayers = 5, 99 | openPublicPlayers = 5, 100 | sessionSettings = new 101 | { 102 | maxPublicPlayers = 100, 103 | maxPrivatePlayers = 100, 104 | shouldAdvertise = true, 105 | allowJoinInProgress = true, 106 | isDedicated = true, 107 | usesStats = true, 108 | allowInvites = true, 109 | usesPresence = false, 110 | allowJoinViaPresence = false, 111 | allowJoinViaPresenceFriendsOnly = false, 112 | buildUniqueId = "build_unique_id", 113 | attributes = "" 114 | } 115 | }; 116 | 117 | Response.StatusCode = 200; 118 | Response.ContentType = "application/json"; 119 | Response.Write(JsonConvert.SerializeObject(response)); 120 | } 121 | 122 | // /fortnite/api/matchmaking/session/`id/heartbeat 123 | // /fortnite/api/matchmaking/session/`id/join?accountId=`accountId 124 | // /fortnite/api/matchmaking/session/`id/start 125 | // /fortnite/api/matchmaking/session/`id/stop 126 | // /fortnite/api/matchmaking/session/`id/invite 127 | // /fortnite/api/matchmaking/session/`id/players 128 | // /fortnite/api/matchmaking/session/`id/matchMakingRequest 129 | } 130 | } -------------------------------------------------------------------------------- /ModniteServer.Api/Controllers/CustomizationController.cs: -------------------------------------------------------------------------------- 1 | using ModniteServer.API.Accounts; 2 | using Newtonsoft.Json; 3 | using Newtonsoft.Json.Linq; 4 | using Serilog; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Text; 8 | 9 | namespace ModniteServer.API.Controllers 10 | { 11 | public sealed class CustomizationController : Controller 12 | { 13 | /// 14 | /// Updates the player's equipped item choice. 15 | /// 16 | [Route("POST", "/fortnite/api/game/v2/profile/*/client/EquipBattleRoyaleCustomization")] 17 | public void EquipBattleRoyaleCustomization() 18 | { 19 | string accountId = Request.Url.Segments[Request.Url.Segments.Length - 3].Replace("/", ""); 20 | 21 | string profileId = Query["profileId"]; 22 | int rvn = Convert.ToInt32(Query["rvn"]); 23 | 24 | byte[] buffer = new byte[Request.ContentLength64]; 25 | Request.InputStream.Read(buffer, 0, buffer.Length); 26 | 27 | var request = JObject.Parse(Encoding.UTF8.GetString(buffer)); 28 | string slotName = (string)request["slotName"]; 29 | string itemToSlot = (string)request["itemToSlot"]; 30 | int indexWithinSlot = (int)request["indexWithinSlot"]; 31 | var variantUpdates = new List(); // TODO: variantUpdates 32 | 33 | string name = ""; 34 | switch (slotName) 35 | { 36 | case "Character": 37 | name = "favorite_character"; 38 | break; 39 | 40 | case "Dance": 41 | name = "favorite_dance"; 42 | break; 43 | 44 | case "Backpack": 45 | name = "favorite_backpack"; 46 | break; 47 | 48 | case "Pickaxe": 49 | name = "favorite_pickaxe"; 50 | break; 51 | 52 | case "Glider": 53 | name = "favorite_glider"; 54 | break; 55 | 56 | case "SkyDiveContrail": 57 | name = "favorite_skydivecontrail"; 58 | break; 59 | 60 | case "MusicPack": 61 | name = "favorite_musicpack"; 62 | break; 63 | 64 | case "LoadingScreen": 65 | name = "favorite_loadingscreen"; 66 | break; 67 | 68 | // TODO: add support for sprays 69 | 70 | default: 71 | Log.Error($"'{accountId}' tried to update unknown item slot '{slotName}' in profile '{profileId}'"); 72 | Response.StatusCode = 500; 73 | return; 74 | } 75 | 76 | object profileChange; 77 | Account account = AccountManager.GetAccount(accountId); 78 | 79 | if (account.EquippedItems == null) 80 | account.EquippedItems = ApiConfig.Current.EquippedItems; // updates previous accounts that don't have that value 81 | 82 | if (name == "favorite_dance") 83 | { 84 | // Dances use an array 85 | string dance = "favorite_dance" + indexWithinSlot; 86 | account.EquippedItems[dance] = itemToSlot; 87 | string[] slots = new string[6]; 88 | for (int i = 0; i < slots.Length; i++) 89 | { 90 | slots[i] = account.EquippedItems["favorite_dance" + i]; 91 | } 92 | slots[indexWithinSlot] = itemToSlot; 93 | profileChange = new 94 | { 95 | changeType = "statModified", 96 | name, 97 | value = slots 98 | }; 99 | } 100 | else 101 | { 102 | account.EquippedItems[name] = itemToSlot; // add it to equipped items 103 | profileChange = new 104 | { 105 | changeType = "statModified", 106 | name, 107 | value = itemToSlot 108 | }; 109 | } 110 | 111 | var response = new 112 | { 113 | profileRevision = rvn + 1, 114 | profileId, 115 | profileChangesBaseRevision = rvn, 116 | profileChanges = new List 117 | { 118 | profileChange 119 | } 120 | }; 121 | 122 | // TODO: update in Account model 123 | Log.Information($"'{accountId}' changed favorite {slotName}"); 124 | 125 | Response.StatusCode = 200; 126 | Response.ContentType = "application/json"; 127 | Response.Write(JsonConvert.SerializeObject(response)); 128 | } 129 | 130 | /// 131 | /// Updates the player's banner. 132 | /// 133 | [Route("POST", "/fortnite/api/game/v2/profile/*/client/SetBattleRoyaleBanner")] 134 | public void EquipBanner() 135 | { 136 | string accountId = Request.Url.Segments[Request.Url.Segments.Length - 3].Replace("/", ""); 137 | 138 | string profileId = Query["profileId"]; 139 | int rvn = Convert.ToInt32(Query["rvn"]); 140 | 141 | byte[] buffer = new byte[Request.ContentLength64]; 142 | Request.InputStream.Read(buffer, 0, buffer.Length); 143 | 144 | var request = JObject.Parse(Encoding.UTF8.GetString(buffer)); 145 | 146 | // TODO: return entire athena profile data 147 | Response.StatusCode = 500; 148 | } 149 | } 150 | } -------------------------------------------------------------------------------- /ModniteServer/Properties/Resources.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | text/microsoft-resx 107 | 108 | 109 | 2.0 110 | 111 | 112 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 113 | 114 | 115 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | -------------------------------------------------------------------------------- /ModniteServer.Services/Matchmaker/MatchmakerServer.cs: -------------------------------------------------------------------------------- 1 | using ModniteServer.Websockets; 2 | using Newtonsoft.Json; 3 | using Serilog; 4 | using System; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace ModniteServer.Matchmaker 9 | { 10 | public sealed class MatchmakerServer : IDisposable 11 | { 12 | private bool _isDisposed; 13 | 14 | private readonly WebsocketServer _server; 15 | 16 | public MatchmakerServer(ushort port) 17 | { 18 | Port = port; 19 | 20 | _server = new WebsocketServer(Port); 21 | _server.NewConnection += OnNewConnection; 22 | _server.MessageReceived += OnMessageReceived; 23 | } 24 | 25 | public ILogger Logger 26 | { 27 | get { return Log.Logger; } 28 | set { Log.Logger = value; } 29 | } 30 | 31 | public ushort Port { get; } 32 | 33 | public void Start() => _server.Start(); 34 | 35 | public void Dispose() 36 | { 37 | if (!_isDisposed) 38 | { 39 | _server.Dispose(); 40 | _isDisposed = true; 41 | } 42 | } 43 | 44 | private void OnNewConnection(object sender, System.Net.Sockets.Socket e) 45 | { 46 | Log.Information("[Matchmaker] New connection"); 47 | } 48 | 49 | private async void OnNewConnection(object sender, WebsocketMessageNewConnectionEventArgs e) 50 | { 51 | // The payload obtained in /fortnite/api/game/v2/matchmakingservice/ticket/player/* 52 | // is supposed to be included in the authorization header, but for some reason it's 53 | // blank. Not a big deal for now, but will need to fix to support multiplayer. 54 | 55 | string[] authParts = e.Authorization.Split(' '); 56 | 57 | // Epic-Signed 58 | string ticketType = authParts[1]; 59 | string payload = authParts[2]; 60 | // signature 61 | 62 | // The client uses a state machine for matchmaking, so we'll have to go through the 63 | // state machine with valid states. For instance, jumping from ConnectingToService 64 | // directly to InQueue skipping other states will cause an error. 65 | 66 | // Changes client state from ObtainingTicket to ConnectingToService 67 | _server.SendMessage(e.Socket, MessageType.Text, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(new 68 | { 69 | payload = new 70 | { 71 | state = "Connecting" 72 | }, 73 | name = "StatusUpdate" 74 | }))); 75 | 76 | await Task.Delay(10); 77 | 78 | // Changes client state from ConnectingToService to WaitingForParty 79 | _server.SendMessage(e.Socket, MessageType.Text, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(new 80 | { 81 | payload = new 82 | { 83 | state = "Waiting", 84 | totalPlayers = 1, 85 | connectedPlayers = 1 86 | }, 87 | name = "StatusUpdate" 88 | }))); 89 | 90 | await Task.Delay(10); 91 | 92 | // Changes client state from WaitingForParty to InQueue 93 | _server.SendMessage(e.Socket, MessageType.Text, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(new 94 | { 95 | payload = new 96 | { 97 | state = "Queued", 98 | queuedPlayers = 1, 99 | estimatedWaitSec = 10, 100 | status = 1, 101 | ticketId = "ticket_id" 102 | }, 103 | name = "StatusUpdate" 104 | }))); 105 | 106 | await Task.Delay(10); 107 | 108 | // Changes client state from InQueue to InReadyCheck 109 | _server.SendMessage(e.Socket, MessageType.Text, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(new 110 | { 111 | payload = new 112 | { 113 | state = "ReadyCheck", 114 | matchId = "match_id", 115 | status = "PASSED", 116 | durationSec = 0, 117 | ready = true, 118 | responded = 1, 119 | totalPlayers = 1, 120 | readyPlayers = 1 121 | }, 122 | name = "StatusUpdate" 123 | }))); 124 | 125 | // Changes client state from InReadyCheck to InSessionAssignment 126 | _server.SendMessage(e.Socket, MessageType.Text, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(new 127 | { 128 | payload = new 129 | { 130 | state = "SessionAssignment", 131 | matchId = "match_id" 132 | }, 133 | name = "StatusUpdate" 134 | }))); 135 | 136 | // Changes client state from InSessionAssignment to Finished 137 | _server.SendMessage(e.Socket, MessageType.Text, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(new 138 | { 139 | payload = new 140 | { 141 | sessionId = "session_id", 142 | joinDelaySec = 1, 143 | matchId = "match_id" 144 | }, 145 | name = "Play" 146 | }))); 147 | } 148 | 149 | private void OnMessageReceived(object sender, WebsocketMessageReceivedEventArgs e) 150 | { 151 | if (e.Message.TextContent == "ping") 152 | { 153 | _server.SendMessage(e.Socket, MessageType.Text, Encoding.UTF8.GetBytes("pong")); 154 | Log.Information("[Matchmaker] Client sent 'ping'. Responded with 'pong'."); 155 | } 156 | } 157 | } 158 | } -------------------------------------------------------------------------------- /ModniteServer.Services/Websockets/WebsocketMessage.cs: -------------------------------------------------------------------------------- 1 | using Serilog; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Text; 6 | 7 | namespace ModniteServer.Websockets 8 | { 9 | internal enum MessageType 10 | { 11 | ContinuationFrame, 12 | Text, 13 | Binary, 14 | Ping, 15 | Pong 16 | } 17 | 18 | internal sealed class WebsocketMessage 19 | { 20 | internal WebsocketMessage(byte[] buffer) 21 | { 22 | IsCompleted = (buffer[0] & 0b_1000_0000) != 0; 23 | 24 | byte opcode = (byte)(buffer[0] & 0b_0000_1111); 25 | bool maskBit = (buffer[1] & 0b_1000_0000) != 0; 26 | int payloadLength = buffer[1] - 128; 27 | 28 | if (!maskBit) 29 | { 30 | // Invalid client message (all client messages must be masked) 31 | //Log.Warning("Invalid Websocket message received from client"); 32 | } 33 | 34 | int maskOffset = 2; 35 | if (payloadLength == 126) 36 | { 37 | payloadLength = (buffer[2] << 8) | buffer[3]; 38 | maskOffset += 2; 39 | } 40 | else if (payloadLength == 127) 41 | { 42 | payloadLength = (buffer[2] << 24) | (buffer[3] << 16) | (buffer[4] << 8) | buffer[5]; 43 | maskOffset += 4; 44 | } 45 | 46 | byte[] mask = BitConverter.GetBytes(BitConverter.ToInt32(buffer, maskOffset)); 47 | 48 | byte[] decoded = new byte[payloadLength]; 49 | for (int i = 0; i < payloadLength; i++) 50 | { 51 | decoded[i] = (byte)(buffer[i + 4 + maskOffset] ^ mask[i % 4]); 52 | } 53 | 54 | switch (opcode) 55 | { 56 | case 0: 57 | MessageType = MessageType.ContinuationFrame; 58 | break; 59 | 60 | case 1: 61 | MessageType = MessageType.Text; 62 | TextContent = Encoding.UTF8.GetString(decoded); 63 | break; 64 | 65 | case 2: 66 | MessageType = MessageType.Binary; 67 | BinaryContent = decoded; 68 | break; 69 | 70 | case 8: // close 71 | // todo 72 | break; 73 | 74 | case 9: // ping 75 | // todo 76 | break; 77 | 78 | case 10: // pong 79 | // todo 80 | break; 81 | 82 | default: 83 | Log.Warning("Invalid opcode received"); 84 | break; 85 | } 86 | } 87 | 88 | internal WebsocketMessage() 89 | { 90 | } 91 | 92 | public MessageType MessageType { get; set; } 93 | 94 | public bool IsCompleted { get; set; } 95 | 96 | public string TextContent { get; set; } 97 | 98 | public byte[] BinaryContent { get; set; } 99 | 100 | internal static WebsocketMessage Defragment(IEnumerable fragments) 101 | { 102 | // TODO 103 | throw new NotImplementedException(); 104 | } 105 | 106 | public byte[] Serialize() 107 | { 108 | using (var stream = new MemoryStream()) 109 | using (var writer = new BinaryWriter(stream)) 110 | { 111 | if (MessageType == MessageType.Text) 112 | { 113 | writer.Write((byte)0b_1000_0001); 114 | 115 | byte[] textBuffer = Encoding.UTF8.GetBytes(TextContent); 116 | if (textBuffer.Length >= 126) 117 | { 118 | if (textBuffer.Length > ushort.MaxValue) 119 | { 120 | writer.Write((byte)0b_0111_1111); 121 | 122 | // TODO: big endian 123 | //writer.Write(textBuffer.Length); 124 | throw new NotImplementedException($"Text content larger than {ushort.MaxValue} is not supported yet"); 125 | } 126 | else 127 | { 128 | writer.Write((byte)0b_0111_1110); 129 | writer.Write((byte)((textBuffer.Length & 0xFF00) >> 8)); 130 | writer.Write((byte)(textBuffer.Length & 0xFF)); 131 | } 132 | } 133 | else 134 | { 135 | writer.Write((byte)textBuffer.Length); 136 | } 137 | 138 | writer.Write(textBuffer); 139 | } 140 | else if (MessageType == MessageType.Binary) 141 | { 142 | if (BinaryContent.Length >= 0b_0111_1110) 143 | { 144 | throw new NotImplementedException("Binary content larger than 127 bytes is not supported yet"); 145 | } 146 | 147 | writer.Write((byte)0b_1000_0010); 148 | writer.Write((byte)BinaryContent.Length); 149 | writer.Write(BinaryContent); 150 | } 151 | else 152 | { 153 | throw new NotImplementedException("Other message types are not supported yet"); 154 | } 155 | 156 | return stream.ToArray(); 157 | } 158 | } 159 | 160 | public override string ToString() 161 | { 162 | if (MessageType == MessageType.Text) 163 | { 164 | return TextContent; 165 | } 166 | else if (MessageType == MessageType.Binary) 167 | { 168 | return BitConverter.ToString(BinaryContent).Replace("-", " "); 169 | } 170 | else 171 | { 172 | return nameof(WebsocketMessage); 173 | } 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /ModniteServer/ModniteServer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {21EE4C5E-3C46-4885-9D30-CE1B497F0908} 8 | WinExe 9 | ModniteServer 10 | ModniteServer 11 | v4.7.2 12 | 512 13 | {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 14 | 4 15 | true 16 | true 17 | 18 | 19 | AnyCPU 20 | true 21 | full 22 | false 23 | bin\Debug\ 24 | DEBUG;TRACE 25 | prompt 26 | 4 27 | 7.2 28 | 29 | 30 | AnyCPU 31 | pdbonly 32 | true 33 | bin\Release\ 34 | TRACE 35 | prompt 36 | 4 37 | 7.2 38 | 39 | 40 | 41 | 42 | False 43 | ..\libs\ModniteServer.API.Core.dll 44 | 45 | 46 | ..\packages\Newtonsoft.Json.11.0.2\lib\net45\Newtonsoft.Json.dll 47 | 48 | 49 | ..\packages\Serilog.2.7.1\lib\net46\Serilog.dll 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 4.0 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | MSBuild:Compile 69 | Designer 70 | 71 | 72 | Designer 73 | MSBuild:Compile 74 | 75 | 76 | Designer 77 | MSBuild:Compile 78 | 79 | 80 | MSBuild:Compile 81 | Designer 82 | 83 | 84 | App.xaml 85 | Code 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | LogStream.xaml 97 | 98 | 99 | LogStreamEntry.xaml 100 | 101 | 102 | 103 | 104 | MainWindow.xaml 105 | Code 106 | 107 | 108 | 109 | 110 | Code 111 | 112 | 113 | True 114 | True 115 | Resources.resx 116 | 117 | 118 | True 119 | Settings.settings 120 | True 121 | 122 | 123 | ResXFileCodeGenerator 124 | Resources.Designer.cs 125 | 126 | 127 | 128 | 129 | SettingsSingleFileGenerator 130 | Settings.Designer.cs 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | {8aa47228-ba31-484f-abb6-c91b5e3ee5be} 139 | ModniteServer.API 140 | 141 | 142 | {6959e822-fe2f-4d13-b3ef-2aa7c8d58ea9} 143 | ModniteServer.Services 144 | 145 | 146 | 147 | 148 | -------------------------------------------------------------------------------- /ModniteServer.Api/Controllers/ClientQuestController.cs: -------------------------------------------------------------------------------- 1 | using ModniteServer.API.Accounts; 2 | using Newtonsoft.Json; 3 | using System; 4 | using System.Collections.Generic; 5 | 6 | namespace ModniteServer.API.Controllers.Profile 7 | { 8 | public sealed class ClientQuestController : Controller 9 | { 10 | [Route("POST", "/fortnite/api/game/v2/profile/*/client/ClientQuestLogin")] 11 | public void ClientQuestLogin() 12 | { 13 | // TODO: Remove duplicate profile code in this and QueryProfile endpoints. 14 | 15 | // Note: Quests are items in the 'athena' profile. 16 | 17 | string accountId = Request.Url.Segments[Request.Url.Segments.Length - 3].Replace("/", ""); 18 | 19 | if (!AccountManager.AccountExists(accountId)) 20 | { 21 | Response.StatusCode = 404; 22 | return; 23 | } 24 | 25 | var account = AccountManager.GetAccount(accountId); 26 | 27 | Query.TryGetValue("profileId", out string profileId); 28 | Query.TryGetValue("rvn", out string rvn); 29 | int revision = Convert.ToInt32(rvn ?? "-2"); 30 | 31 | var items = account.AthenaItems; 32 | var itemsFormatted = new Dictionary(); 33 | foreach (string item in items) 34 | { 35 | var itemGuid = item; // Makes life easier - config file doesn't store numbers anymore, and it can be read anywhere. 36 | itemsFormatted.Add(itemGuid, new 37 | { 38 | templateId = item, 39 | attributes = new 40 | { 41 | max_level_bonus = 0, 42 | level = 1, 43 | item_seen = 1, 44 | xp = 0, 45 | variants = new List(), 46 | favorite = false 47 | }, 48 | quantity = 1 49 | }); 50 | } 51 | 52 | string[] dances = new string[6]; 53 | for (int i = 0; i < dances.Length; i++) 54 | { 55 | dances[i] = account.EquippedItems["favorite_dance" + i]; 56 | } 57 | 58 | var response = new 59 | { 60 | profileRevision = 10, 61 | profileId = "athena", 62 | profileChangesBaseRevision = 8, 63 | profileChanges = new List 64 | { 65 | new 66 | { 67 | changeType = "fullProfileUpdate", 68 | profile = new 69 | { 70 | _id = accountId, // not really account id but idk 71 | created = DateTime.Now.AddDays(-7).ToDateTimeString(), 72 | updated = DateTime.Now.AddDays(-1).ToDateTimeString(), 73 | rvn = 10, 74 | wipeNumber = 5, 75 | accountId, 76 | profileId = "athena", 77 | version = "fortnitemares_part4_fixup_oct_18", 78 | items = itemsFormatted, 79 | stats = new 80 | { 81 | attributes = new 82 | { 83 | past_seasons = new string[0], 84 | season_match_boost = 1000, 85 | favorite_victorypose = "", 86 | mfa_reward_claimed = false, 87 | quest_manager = new 88 | { 89 | dailyLoginInterval = DateTime.Now.AddDays(1).ToDateTimeString(), 90 | dailyQuestRerolls = 1 91 | }, 92 | book_level = 0, 93 | season_num = 0, 94 | favorite_consumableemote = "", 95 | banner_color = "defaultcolor1", 96 | favorite_callingcard = "", 97 | favorite_character = account.EquippedItems["favorite_character"], 98 | favorite_spray = new string[0], 99 | book_xp = 0, 100 | favorite_loadingscreen = account.EquippedItems["favorite_loadingscreen"], 101 | book_purchased = false, 102 | lifetime_wins = 0, 103 | favorite_hat = "", 104 | level = 1000000, 105 | favorite_battlebus = "", 106 | favorite_mapmarker = "", 107 | favorite_vehicledeco = "", 108 | accountLevel = 1000000, 109 | favorite_backpack = account.EquippedItems["favorite_backpack"], 110 | favorite_dance = dances, 111 | inventory_limit_bonus = 0, 112 | favorite_skydivecontrail = account.EquippedItems["favorite_skydivecontrail"], 113 | favorite_pickaxe = account.EquippedItems["favorite_pickaxe"], 114 | favorite_glider = account.EquippedItems["favorite_glider"], 115 | daily_rewards = new { }, 116 | xp = 0, 117 | season_friend_match_boost = 0, 118 | favorite_musicpack = account.EquippedItems["favorite_musicpack"], 119 | banner_icon = "standardbanner1" 120 | } 121 | }, 122 | commandRevision = 5 123 | } 124 | } 125 | }, 126 | profileCommandRevision = 1, 127 | serverTime = DateTime.Now.ToDateTimeString(), 128 | multiUpdate = new [] 129 | { 130 | new 131 | { 132 | profileRevision = 10, 133 | profileId = "common_core", 134 | profileChangesBaseRevision = 8, 135 | profileCommandRevision = 5, 136 | profileChanges = new List() 137 | } 138 | }, 139 | responseVersion = 1 140 | }; 141 | 142 | Response.StatusCode = 200; 143 | Response.ContentType = "application/json"; 144 | Response.Write(JsonConvert.SerializeObject(response)); 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /ModniteServer.Api/Controllers/ContentController.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.Collections.Generic; 4 | 5 | namespace ModniteServer.API.Controllers 6 | { 7 | public sealed class ContentController : Controller 8 | { 9 | [Route("GET", "/content/api/pages/fortnite-game")] 10 | public void GetNews() 11 | { 12 | var response = new 13 | { 14 | _title = "Fortnite Game", 15 | _activeDate = DateTime.Now.AddDays(-1).ToDateTimeString(), 16 | lastModified = DateTime.Now.ToDateTimeString(), 17 | _locale = "en-US", 18 | subgameselectdata = new 19 | { 20 | _activeDate = DateTime.Now.AddDays(-1).ToDateTimeString(), 21 | lastModified = DateTime.Now.ToDateTimeString(), 22 | _locale = "en-US", 23 | title = "subgameselectdata", 24 | saveTheWorldUnowned = new 25 | { 26 | _type = "CommonUI Simple Message", 27 | message = new 28 | { 29 | image = "https://modniteimages.azurewebsites.net/modnite-selectgame.png", 30 | hidden = false, 31 | messagetype = "normal", 32 | _type = "CommonUI Simple Message Base", 33 | title = "Co-op PVE", 34 | body = "Cooperative PvE storm-fighting adventure!", 35 | spotlight = false 36 | } 37 | }, 38 | saveTheWorld = new 39 | { 40 | _type = "CommonUI Simple Message", 41 | message = new 42 | { 43 | image = "https://modniteimages.azurewebsites.net/modnite-selectgame.png", 44 | hidden = false, 45 | messagetype = "normal", 46 | _type = "CommonUI Simple Message Base", 47 | title = "Co-op PVE", 48 | body = "Cooperative PvE storm-fighting adventure!", 49 | spotlight = false 50 | } 51 | }, 52 | battleRoyale = new 53 | { 54 | _type = "CommonUI Simple Message", 55 | message = new 56 | { 57 | image = "https://modniteimages.azurewebsites.net/modnite-selectgame.png", 58 | hidden = false, 59 | messageType = "normal", 60 | title = "1 Player PvP", 61 | body = "Single Player Battle Royale", 62 | spotlight = true 63 | } 64 | } 65 | }, 66 | loginmessage = new 67 | { 68 | _activeDate = DateTime.Now.AddDays(-1).ToDateTimeString(), 69 | lastModified = DateTime.Now.ToDateTimeString(), 70 | _locale = "en-US", 71 | _title = "LoginMessage", 72 | loginmessage = new 73 | { 74 | _type = "CommonUI Simple Message Base", 75 | title = "", 76 | body = "" 77 | } 78 | }, 79 | playlistimages = new 80 | { 81 | _activeDate = DateTime.Now.AddDays(-1).ToDateTimeString(), 82 | lastModified = DateTime.Now.ToDateTimeString(), 83 | _locale = "en-US", 84 | _title = "playlistimages", 85 | playlistimages = new 86 | { 87 | images = new List 88 | { 89 | new 90 | { 91 | image = "https://cdn2.unrealengine.com/Fortnite/fortnite-game/playlistinformation/LlamaLower-512x512-5fda7d81162d9d918e72d8deadedc280959a8d1b.png", 92 | _type = "PlaylistImageEntry", 93 | playlistname = "Playlist_Playground" 94 | } 95 | } 96 | } 97 | }, 98 | playlistinformation = new 99 | { 100 | _activeDate = DateTime.Now.AddDays(-1).ToDateTimeString(), 101 | lastModified = DateTime.Now.ToDateTimeString(), 102 | _locale = "en-US", 103 | _title = "playlistinformation", 104 | frontend_matchmaking_header_style = "None", 105 | frontend_matchmaking_header_text = "", 106 | playlist_info = new 107 | { 108 | _type = "Playlist Information", 109 | playlists = new List 110 | { 111 | new 112 | { 113 | image = "https://cdn2.unrealengine.com/Fortnite/fortnite-game/playlistinformation/LlamaLower-512x512-5fda7d81162d9d918e72d8deadedc280959a8d1b.png", 114 | playlist_name = "Playlist_Playground", 115 | special_border = "None", 116 | _type = "FortPlaylistInfo" 117 | } 118 | } 119 | } 120 | }, 121 | tournamentinformation = new 122 | { 123 | _activeDate = DateTime.Now.AddDays(-1).ToDateTimeString(), 124 | lastModified = DateTime.Now.ToDateTimeString(), 125 | _locale = "en-US", 126 | _title = "tournamentinformation", 127 | tournament_info = new 128 | { 129 | tournaments = new List(), 130 | _type = "Tournaments Info" 131 | } 132 | }, 133 | emergencynotice = new 134 | { 135 | news = new 136 | { 137 | _type = "Battle Royale News", 138 | messages = new List() 139 | }, 140 | _title = "emergencynotice", 141 | _activeDate = DateTime.Now.AddDays(-1).ToDateTimeString(), 142 | lastModified = DateTime.Now.ToDateTimeString(), 143 | _locale = "en-US" 144 | } 145 | }; 146 | 147 | Response.StatusCode = 200; 148 | Response.ContentType = "application/json"; 149 | Response.Write(JsonConvert.SerializeObject(response)); 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | **/Properties/launchSettings.json 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.iobj 68 | *.pch 69 | *.pdb 70 | *.ipdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *.log 81 | *.vspscc 82 | *.vssscc 83 | .builds 84 | *.pidb 85 | *.svclog 86 | *.scc 87 | 88 | # Chutzpah Test files 89 | _Chutzpah* 90 | 91 | # Visual C++ cache files 92 | ipch/ 93 | *.aps 94 | *.ncb 95 | *.opendb 96 | *.opensdf 97 | *.sdf 98 | *.cachefile 99 | *.VC.db 100 | *.VC.VC.opendb 101 | 102 | # Visual Studio profiler 103 | *.psess 104 | *.vsp 105 | *.vspx 106 | *.sap 107 | 108 | # Visual Studio Trace Files 109 | *.e2e 110 | 111 | # TFS 2012 Local Workspace 112 | $tf/ 113 | 114 | # Guidance Automation Toolkit 115 | *.gpState 116 | 117 | # ReSharper is a .NET coding add-in 118 | _ReSharper*/ 119 | *.[Rr]e[Ss]harper 120 | *.DotSettings.user 121 | 122 | # JustCode is a .NET coding add-in 123 | .JustCode 124 | 125 | # TeamCity is a build add-in 126 | _TeamCity* 127 | 128 | # DotCover is a Code Coverage Tool 129 | *.dotCover 130 | 131 | # AxoCover is a Code Coverage Tool 132 | .axoCover/* 133 | !.axoCover/settings.json 134 | 135 | # Visual Studio code coverage results 136 | *.coverage 137 | *.coveragexml 138 | 139 | # NCrunch 140 | _NCrunch_* 141 | .*crunch*.local.xml 142 | nCrunchTemp_* 143 | 144 | # MightyMoose 145 | *.mm.* 146 | AutoTest.Net/ 147 | 148 | # Web workbench (sass) 149 | .sass-cache/ 150 | 151 | # Installshield output folder 152 | [Ee]xpress/ 153 | 154 | # DocProject is a documentation generator add-in 155 | DocProject/buildhelp/ 156 | DocProject/Help/*.HxT 157 | DocProject/Help/*.HxC 158 | DocProject/Help/*.hhc 159 | DocProject/Help/*.hhk 160 | DocProject/Help/*.hhp 161 | DocProject/Help/Html2 162 | DocProject/Help/html 163 | 164 | # Click-Once directory 165 | publish/ 166 | 167 | # Publish Web Output 168 | *.[Pp]ublish.xml 169 | *.azurePubxml 170 | # Note: Comment the next line if you want to checkin your web deploy settings, 171 | # but database connection strings (with potential passwords) will be unencrypted 172 | *.pubxml 173 | *.publishproj 174 | 175 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 176 | # checkin your Azure Web App publish settings, but sensitive information contained 177 | # in these scripts will be unencrypted 178 | PublishScripts/ 179 | 180 | # NuGet Packages 181 | *.nupkg 182 | # The packages folder can be ignored because of Package Restore 183 | **/[Pp]ackages/* 184 | # except build/, which is used as an MSBuild target. 185 | !**/[Pp]ackages/build/ 186 | # Uncomment if necessary however generally it will be regenerated when needed 187 | #!**/[Pp]ackages/repositories.config 188 | # NuGet v3's project.json files produces more ignorable files 189 | *.nuget.props 190 | *.nuget.targets 191 | 192 | # Microsoft Azure Build Output 193 | csx/ 194 | *.build.csdef 195 | 196 | # Microsoft Azure Emulator 197 | ecf/ 198 | rcf/ 199 | 200 | # Windows Store app package directories and files 201 | AppPackages/ 202 | BundleArtifacts/ 203 | Package.StoreAssociation.xml 204 | _pkginfo.txt 205 | *.appx 206 | 207 | # Visual Studio cache files 208 | # files ending in .cache can be ignored 209 | *.[Cc]ache 210 | # but keep track of directories ending in .cache 211 | !*.[Cc]ache/ 212 | 213 | # Others 214 | ClientBin/ 215 | ~$* 216 | *~ 217 | *.dbmdl 218 | *.dbproj.schemaview 219 | *.jfm 220 | *.pfx 221 | *.publishsettings 222 | orleans.codegen.cs 223 | 224 | # Including strong name files can present a security risk 225 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 226 | #*.snk 227 | 228 | # Since there are multiple workflows, uncomment next line to ignore bower_components 229 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 230 | #bower_components/ 231 | 232 | # RIA/Silverlight projects 233 | Generated_Code/ 234 | 235 | # Backup & report files from converting an old project file 236 | # to a newer Visual Studio version. Backup files are not needed, 237 | # because we have git ;-) 238 | _UpgradeReport_Files/ 239 | Backup*/ 240 | UpgradeLog*.XML 241 | UpgradeLog*.htm 242 | ServiceFabricBackup/ 243 | *.rptproj.bak 244 | 245 | # SQL Server files 246 | *.mdf 247 | *.ldf 248 | *.ndf 249 | 250 | # Business Intelligence projects 251 | *.rdl.data 252 | *.bim.layout 253 | *.bim_*.settings 254 | *.rptproj.rsuser 255 | 256 | # Microsoft Fakes 257 | FakesAssemblies/ 258 | 259 | # GhostDoc plugin setting file 260 | *.GhostDoc.xml 261 | 262 | # Node.js Tools for Visual Studio 263 | .ntvs_analysis.dat 264 | node_modules/ 265 | 266 | # Visual Studio 6 build log 267 | *.plg 268 | 269 | # Visual Studio 6 workspace options file 270 | *.opt 271 | 272 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 273 | *.vbw 274 | 275 | # Visual Studio LightSwitch build output 276 | **/*.HTMLClient/GeneratedArtifacts 277 | **/*.DesktopClient/GeneratedArtifacts 278 | **/*.DesktopClient/ModelManifest.xml 279 | **/*.Server/GeneratedArtifacts 280 | **/*.Server/ModelManifest.xml 281 | _Pvt_Extensions 282 | 283 | # Paket dependency manager 284 | .paket/paket.exe 285 | paket-files/ 286 | 287 | # FAKE - F# Make 288 | .fake/ 289 | 290 | # JetBrains Rider 291 | .idea/ 292 | *.sln.iml 293 | 294 | # CodeRush 295 | .cr/ 296 | 297 | # Python Tools for Visual Studio (PTVS) 298 | __pycache__/ 299 | *.pyc 300 | 301 | # Cake - Uncomment if you are using it 302 | # tools/** 303 | # !tools/packages.config 304 | 305 | # Tabs Studio 306 | *.tss 307 | 308 | # Telerik's JustMock configuration file 309 | *.jmconfig 310 | 311 | # BizTalk build output 312 | *.btp.cs 313 | *.btm.cs 314 | *.odx.cs 315 | *.xsd.cs 316 | 317 | # OpenCover UI analysis results 318 | OpenCover/ 319 | 320 | # Azure Stream Analytics local run output 321 | ASALocalRun/ 322 | 323 | # MSBuild Binary and Structured Log 324 | *.binlog 325 | 326 | # NVidia Nsight GPU debugger configuration file 327 | *.nvuser 328 | 329 | # MFractors (Xamarin productivity tool) working folder 330 | .mfractor/ 331 | -------------------------------------------------------------------------------- /ModniteServer.Api/Controllers/OAuthController.cs: -------------------------------------------------------------------------------- 1 | using ModniteServer.API.Accounts; 2 | using ModniteServer.API.OAuth; 3 | using Newtonsoft.Json; 4 | using Serilog; 5 | using System; 6 | using System.Linq; 7 | using System.Security.Cryptography; 8 | using System.Text; 9 | 10 | namespace ModniteServer.API.Controllers 11 | { 12 | /// 13 | /// Handles requests for generating OAuth tokens. This implementation is good enough for a small 14 | /// server where you know everybody. You should rewrite this class if you want a solidly secure 15 | /// authentication system. 16 | /// 17 | public sealed class OAuthController : Controller 18 | { 19 | public const string FortniteClientId = "ec684b8c687f479fadea3cb2ad83f5c6:e1f31c211f28413186262d37a13fc84d"; 20 | 21 | public static readonly TimeSpan ClientAccessTokenExpiry = TimeSpan.FromHours(4); 22 | public static readonly TimeSpan UserAccessTokenExpiry = TimeSpan.FromHours(8); 23 | 24 | /// 25 | /// Generates an OAuth token when provided with valid credentials. 26 | /// 27 | [Route("POST", "/account/api/oauth/token")] 28 | public void GenerateToken() 29 | { 30 | Query.TryGetValue("grant_type", out string grantType); 31 | switch (grantType) 32 | { 33 | case "client_credentials": 34 | GenerateTokenFromClientId(); 35 | return; 36 | 37 | case "password": 38 | GenerateTokenFromCredentials(); 39 | return; 40 | 41 | // Exchange code is used if the user had logged in via the Epic Games Launcher. 42 | // However, we don't have a launcher so there is no reason to support this. 43 | case "exchange_code": 44 | default: 45 | Response.StatusCode = 403; 46 | return; 47 | } 48 | } 49 | 50 | /// 51 | /// Validates the provided token. 52 | /// 53 | [Route("GET", "/account/api/oauth/verify")] 54 | public void VerifyToken() 55 | { 56 | if (Authorize()) 57 | { 58 | Response.StatusCode = 204; 59 | } 60 | else 61 | { 62 | Response.StatusCode = 403; 63 | } 64 | } 65 | 66 | /// 67 | /// Invalidates the provided token. 68 | /// 69 | [Route("DELETE", "/account/api/oauth/sessions/kill/*")] 70 | public void KillSession() 71 | { 72 | string token = Request.Url.Segments.Last(); 73 | OAuthManager.DestroyToken(token); 74 | Response.StatusCode = 204; 75 | } 76 | 77 | /// 78 | /// Generates an OAuth token for an user from their credentials. 79 | /// 80 | private void GenerateTokenFromCredentials() 81 | { 82 | bool hasValidAuth = false; 83 | 84 | string email = Query["username"]; 85 | string password = Query["password"]; 86 | 87 | string passwordHash; 88 | using (var sha256 = new SHA256Managed()) 89 | { 90 | byte[] hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(password)); 91 | var hashString = new StringBuilder(); 92 | foreach (byte b in hash) 93 | { 94 | hashString.AppendFormat("{0:x2}", b); 95 | } 96 | passwordHash = hashString.ToString(); 97 | } 98 | 99 | Account account = null; 100 | if (AccountManager.AccountExists(email)) 101 | { 102 | account = AccountManager.GetAccount(email); 103 | if (account.PasswordHash == passwordHash) 104 | { 105 | if (!account.IsBanned) 106 | { 107 | Log.Information($"{account.DisplayName} logged in {{DisplayName}}{{AccountId}}", account.DisplayName, account.AccountId); 108 | hasValidAuth = true; 109 | } 110 | else 111 | { 112 | Log.Information($"{account.DisplayName} tried to log in but was banned {{DisplayName}}{{AccountId}}", account.DisplayName, account.AccountId); 113 | } 114 | 115 | account.LastLogin = DateTime.UtcNow; 116 | } 117 | } 118 | else if (ApiConfig.Current.AutoCreateAccounts) 119 | { 120 | account = AccountManager.CreateAccount(email, passwordHash); 121 | hasValidAuth = true; 122 | } 123 | 124 | if (hasValidAuth) 125 | { 126 | var token = OAuthManager.CreateToken((int)UserAccessTokenExpiry.TotalSeconds); 127 | 128 | var response = new 129 | { 130 | access_token = token.Token, 131 | expires_in = token.ExpiresIn, 132 | expires_at = token.ExpiresAt.ToDateTimeString(), 133 | token_type = "bearer", 134 | refresh_token = token.Token, // I know, I know... 135 | refresh_expires = token.ExpiresIn, 136 | refresh_expires_at = token.ExpiresAt.ToDateTimeString(), 137 | account_id = account.AccountId, 138 | client_id = FortniteClientId.Split(':')[0], 139 | internal_client = true, 140 | client_service = "fortnite", 141 | app = "fortnite", 142 | in_app_id = account.AccountId 143 | }; 144 | 145 | Response.StatusCode = 200; 146 | Response.ContentType = "application/json"; 147 | Response.Write(JsonConvert.SerializeObject(response)); 148 | } 149 | else 150 | { 151 | Response.StatusCode = 403; 152 | } 153 | } 154 | 155 | /// 156 | /// Generates an OAuth token for the client (not user). 157 | /// 158 | private void GenerateTokenFromClientId() 159 | { 160 | bool hasValidAuth = false; 161 | 162 | string authorization = Request.Headers["Authorization"]; 163 | if (authorization != null) 164 | { 165 | if (authorization.StartsWith("basic ")) 166 | { 167 | string token = authorization.Split(' ')[1]; 168 | token = Encoding.ASCII.GetString(Convert.FromBase64String(token)); 169 | 170 | if (token == FortniteClientId) 171 | { 172 | hasValidAuth = true; 173 | } 174 | } 175 | } 176 | 177 | if (hasValidAuth) 178 | { 179 | var token = OAuthManager.CreateToken((int)ClientAccessTokenExpiry.TotalSeconds); 180 | 181 | var response = new 182 | { 183 | access_token = token.Token, 184 | expires_in = token.ExpiresIn, 185 | expires_at = token.ExpiresAt.ToDateTimeString(), 186 | token_type = "bearer", 187 | client_id = FortniteClientId.Split(':')[0], 188 | internal_client = true, 189 | client_service = "fortnite" 190 | }; 191 | 192 | Response.StatusCode = 200; 193 | Response.ContentType = "application/json"; 194 | Response.Write(JsonConvert.SerializeObject(response)); 195 | } 196 | else 197 | { 198 | Response.StatusCode = 403; 199 | } 200 | } 201 | } 202 | } -------------------------------------------------------------------------------- /ModniteServer.Services/Websockets/WebsocketServer.cs: -------------------------------------------------------------------------------- 1 | using Serilog; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Net; 5 | using System.Net.Sockets; 6 | using System.Security.Cryptography; 7 | using System.Text; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | 11 | namespace ModniteServer.Websockets 12 | { 13 | internal sealed class WebsocketServer : IDisposable 14 | { 15 | private bool _isDisposed; 16 | private bool _serverStarted; 17 | private List _messageFragments; 18 | 19 | private readonly TcpListener _server; 20 | private readonly CancellationTokenSource _cts; 21 | 22 | public WebsocketServer(ushort port) 23 | { 24 | Port = port; 25 | 26 | _server = new TcpListener(IPAddress.Parse("127.0.0.1"), port); 27 | _server.Server.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true); 28 | 29 | _cts = new CancellationTokenSource(); 30 | _messageFragments = new List(); 31 | } 32 | 33 | public event EventHandler MessageReceived; 34 | 35 | public event EventHandler NewConnection; 36 | 37 | public ushort Port { get; } 38 | 39 | public void Start() 40 | { 41 | if (_serverStarted) 42 | throw new InvalidOperationException("Server already started"); 43 | 44 | _server.Start(); 45 | _serverStarted = true; 46 | 47 | Task.Run(AcceptConnectionsAsync); 48 | } 49 | 50 | public void Dispose() 51 | { 52 | if (!_isDisposed) 53 | { 54 | using (_cts) _cts.Cancel(); 55 | 56 | _server.Stop(); 57 | _isDisposed = true; 58 | } 59 | } 60 | 61 | public void SendMessage(Socket socket, MessageType messageType, byte[] data) 62 | { 63 | var message = new WebsocketMessage { MessageType = messageType }; 64 | if (messageType == MessageType.Text) 65 | { 66 | message.TextContent = Encoding.UTF8.GetString(data); 67 | } 68 | else if (messageType == MessageType.Binary) 69 | { 70 | message.BinaryContent = data; 71 | } 72 | 73 | string dataString; 74 | if (messageType == MessageType.Text) 75 | { 76 | dataString = message.TextContent; 77 | } 78 | else 79 | { 80 | dataString = BitConverter.ToString(data).Replace("-", " "); 81 | } 82 | 83 | Log.Information($"Sent {data.Length} bytes {{Client}}{{MessageType}}{{Message}}", socket.RemoteEndPoint.ToString(), messageType, dataString); 84 | using (var stream = new NetworkStream(socket, false)) 85 | { 86 | byte[] buffer = message.Serialize(); 87 | stream.Write(buffer, 0, buffer.Length); 88 | stream.Flush(); 89 | } 90 | } 91 | 92 | internal void SendMessage(Socket socket, MessageType text, object p) 93 | { 94 | throw new NotImplementedException(); 95 | } 96 | 97 | private async Task AcceptConnectionsAsync() 98 | { 99 | while (!_cts.IsCancellationRequested) 100 | { 101 | var client = await _server.AcceptSocketAsync(); 102 | EstablishConnection(client); 103 | } 104 | } 105 | 106 | private void EstablishConnection(Socket client) 107 | { 108 | Task.Run(async () => 109 | { 110 | var stream = new NetworkStream(client); 111 | 112 | // Handshake/upgrade connection 113 | while (!stream.DataAvailable && !_cts.IsCancellationRequested) { } 114 | _cts.Token.ThrowIfCancellationRequested(); 115 | 116 | byte[] buffer = new byte[client.Available]; 117 | await stream.ReadAsync(buffer, 0, buffer.Length, _cts.Token); 118 | 119 | // GET // HTTP/1.1 120 | // Pragma: no-cache 121 | // Cache-Control: no-cache 122 | // Host: 127.0.0.1 123 | // Origin: http://127.0.0.1 124 | // Upgrade: websocket 125 | // Connection: Upgrade 126 | // Sec-WebSocket-Key: 127 | // Sec-WebSocket-Protocol: xmpp 128 | // Sec-WebSocket-Version: 13 129 | 130 | string wsKeyHash = null; 131 | string authorization = null; 132 | string[] request = Encoding.UTF8.GetString(buffer).Split(new [] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries); 133 | foreach (string line in request) 134 | { 135 | if (line.StartsWith("Sec-WebSocket-Key")) 136 | { 137 | string wsKey = line.Substring(line.IndexOf(' ') + 1); 138 | wsKey += "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; // magic 139 | wsKeyHash = Convert.ToBase64String(SHA1.Create().ComputeHash(Encoding.UTF8.GetBytes(wsKey))); 140 | } 141 | 142 | if (line.StartsWith("Authorization")) 143 | { 144 | authorization = line.Substring(line.IndexOf(' ') + 1); 145 | } 146 | } 147 | 148 | var responseBuilder = new StringBuilder(); 149 | responseBuilder.Append("HTTP/1.1 101 Switching Protocols\r\n"); 150 | responseBuilder.Append("Connection: upgrade\r\n"); 151 | responseBuilder.Append("Upgrade: websocket\r\n"); 152 | responseBuilder.Append("Sec-WebSocket-Accept: " + wsKeyHash + "\r\n"); 153 | responseBuilder.Append("\r\n"); 154 | byte[] response = Encoding.UTF8.GetBytes(responseBuilder.ToString()); 155 | await stream.WriteAsync(response, 0, response.Length); 156 | await stream.FlushAsync(); 157 | 158 | // Since we'll be using this task to receive incoming messages, we'll raise the 159 | // new connection event in a different task. 160 | var raiseEventTask = Task.Run(() => 161 | { 162 | NewConnection?.Invoke(this, new WebsocketMessageNewConnectionEventArgs(client, authorization)); 163 | }); 164 | 165 | // Handshake complete, now decode incoming messages. 166 | while (!_cts.IsCancellationRequested) 167 | { 168 | while (!stream.DataAvailable && !_cts.IsCancellationRequested) { } 169 | _cts.Token.ThrowIfCancellationRequested(); 170 | 171 | buffer = new byte[client.Available]; 172 | await stream.ReadAsync(buffer, 0, buffer.Length, _cts.Token); 173 | 174 | string byteString = BitConverter.ToString(buffer).Replace("-", " "); 175 | 176 | var message = new WebsocketMessage(buffer); 177 | if (_messageFragments.Count > 0) 178 | { 179 | _messageFragments.Add(message); 180 | 181 | if (message.IsCompleted) 182 | { 183 | // Combine all message fragments. 184 | message = WebsocketMessage.Defragment(_messageFragments); 185 | _messageFragments.Clear(); 186 | 187 | MessageReceived?.Invoke(this, new WebsocketMessageReceivedEventArgs(client, message)); 188 | } 189 | } 190 | else 191 | { 192 | if (message.IsCompleted) 193 | { 194 | MessageReceived?.Invoke(this, new WebsocketMessageReceivedEventArgs(client, message)); 195 | } 196 | else 197 | { 198 | _messageFragments.Add(message); 199 | } 200 | } 201 | } 202 | }); 203 | } 204 | } 205 | } -------------------------------------------------------------------------------- /ModniteServer.Api/ApiConfig.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Serilog; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Reflection; 7 | 8 | namespace ModniteServer 9 | { 10 | public class ApiConfig 11 | { 12 | public const string ConfigFile = @"\config.json"; 13 | 14 | public const ushort DefaultApiPort = 60101; 15 | public const ushort DefaultXmppPort = 443; 16 | public const ushort DefaultMatchmakerPort = 60103; 17 | 18 | static ApiConfig() 19 | { 20 | string location = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); 21 | string configPath = location + ConfigFile; 22 | 23 | if (!File.Exists(configPath)) 24 | { 25 | Log.Warning("Config file is missing, so a default config was created. {Path}", configPath); 26 | 27 | string json = JsonConvert.SerializeObject(new ApiConfig(), Formatting.Indented); 28 | File.WriteAllText(configPath, json); 29 | } 30 | 31 | Current = JsonConvert.DeserializeObject(File.ReadAllText(configPath)); 32 | 33 | if (Current.AutoCreateAccounts) 34 | Log.Information("New accounts will be automatically created"); 35 | 36 | Log.Information($"Accepting clients on {Current.MinimumVersion.Major}.{Current.MinimumVersion.Minor} or higher {{BuildString}}", 37 | $"++Fortnite+Release-{Current.MinimumVersion.Major}.{Current.MinimumVersion.Minor}-CL-{Current.MinimumVersion.Build}"); 38 | } 39 | 40 | /// 41 | /// Constructs a default config. 42 | /// 43 | private ApiConfig() 44 | { 45 | Port = DefaultApiPort; 46 | XmppPort = DefaultXmppPort; 47 | MatchmakerPort = DefaultMatchmakerPort; 48 | 49 | AutoCreateAccounts = true; 50 | MinimumVersionString = "6.10.4464155"; 51 | 52 | DefaultAthenaItems = new HashSet 53 | { 54 | "AthenaDance:eid_dancemoves", 55 | "AthenaGlider:defaultglider", 56 | "AthenaPickaxe:defaultpickaxe", 57 | "AthenaCharacter:CID_009_Athena_Commando_M", 58 | "AthenaCharacter:CID_010_Athena_Commando_M", 59 | "AthenaCharacter:CID_011_Athena_Commando_M", 60 | "AthenaCharacter:CID_012_Athena_Commando_M", 61 | "AthenaCharacter:CID_013_Athena_Commando_F", 62 | "AthenaCharacter:CID_014_Athena_Commando_F", 63 | "AthenaCharacter:CID_015_Athena_Commando_F", 64 | "AthenaCharacter:CID_016_Athena_Commando_F", 65 | }; 66 | 67 | EquippedItems = new Dictionary 68 | { 69 | {"favorite_character",""}, 70 | {"favorite_backpack",""}, 71 | {"favorite_pickaxe",""}, 72 | {"favorite_glider",""}, 73 | {"favorite_skydivecontrail",""}, 74 | {"favorite_dance0",""}, 75 | {"favorite_dance1",""}, 76 | {"favorite_dance2",""}, 77 | {"favorite_dance3",""}, 78 | {"favorite_dance4",""}, 79 | {"favorite_dance5",""}, 80 | {"favorite_musicpack","" }, 81 | {"favorite_loadingscreen",""}, 82 | }; 83 | 84 | DefaultCoreItems = new HashSet 85 | { 86 | "HomebaseBannerColor:defaultcolor1", 87 | "HomebaseBannerColor:defaultcolor2", 88 | "HomebaseBannerColor:defaultcolor3", 89 | "HomebaseBannerColor:defaultcolor4", 90 | "HomebaseBannerColor:defaultcolor5", 91 | "HomebaseBannerColor:defaultcolor6", 92 | "HomebaseBannerColor:defaultcolor7", 93 | "HomebaseBannerColor:defaultcolor8", 94 | "HomebaseBannerColor:defaultcolor9", 95 | "HomebaseBannerColor:defaultcolor10", 96 | "HomebaseBannerColor:defaultcolor11", 97 | "HomebaseBannerColor:defaultcolor12", 98 | "HomebaseBannerColor:defaultcolor13", 99 | "HomebaseBannerColor:defaultcolor14", 100 | "HomebaseBannerColor:defaultcolor15", 101 | "HomebaseBannerColor:defaultcolor16", 102 | "HomebaseBannerColor:defaultcolor17", 103 | "HomebaseBannerColor:defaultcolor18", 104 | "HomebaseBannerColor:defaultcolor19", 105 | "HomebaseBannerColor:defaultcolor20", 106 | "HomebaseBannerColor:defaultcolor21", 107 | 108 | "HomebaseBannerIcon:standardbanner1", 109 | "HomebaseBannerIcon:standardbanner2", 110 | "HomebaseBannerIcon:standardbanner3", 111 | "HomebaseBannerIcon:standardbanner4", 112 | "HomebaseBannerIcon:standardbanner5", 113 | "HomebaseBannerIcon:standardbanner6", 114 | "HomebaseBannerIcon:standardbanner7", 115 | "HomebaseBannerIcon:standardbanner8", 116 | "HomebaseBannerIcon:standardbanner9", 117 | "HomebaseBannerIcon:standardbanner10", 118 | "HomebaseBannerIcon:standardbanner11", 119 | "HomebaseBannerIcon:standardbanner12", 120 | "HomebaseBannerIcon:standardbanner13", 121 | "HomebaseBannerIcon:standardbanner14", 122 | "HomebaseBannerIcon:standardbanner15", 123 | "HomebaseBannerIcon:standardbanner16", 124 | "HomebaseBannerIcon:standardbanner17", 125 | "HomebaseBannerIcon:standardbanner18", 126 | "HomebaseBannerIcon:standardbanner19", 127 | "HomebaseBannerIcon:standardbanner20", 128 | "HomebaseBannerIcon:standardbanner21", 129 | "HomebaseBannerIcon:standardbanner22", 130 | "HomebaseBannerIcon:standardbanner23", 131 | "HomebaseBannerIcon:standardbanner24", 132 | "HomebaseBannerIcon:standardbanner25", 133 | "HomebaseBannerIcon:standardbanner26", 134 | "HomebaseBannerIcon:standardbanner27", 135 | "HomebaseBannerIcon:standardbanner28", 136 | "HomebaseBannerIcon:standardbanner29", 137 | "HomebaseBannerIcon:standardbanner30", 138 | "HomebaseBannerIcon:standardbanner31" 139 | }; 140 | 141 | #if DEBUG 142 | LogHttpRequests = true; 143 | Log404 = true; 144 | #endif 145 | 146 | ClientEvents = new List(); 147 | } 148 | 149 | public static ApiConfig Current { get; } 150 | 151 | [JsonIgnore] 152 | public ILogger Logger 153 | { 154 | get { return Log.Logger; } 155 | set { Log.Logger = value; } 156 | } 157 | 158 | [JsonProperty(PropertyName = "MinimumVersion")] 159 | public string MinimumVersionString { get; set; } 160 | 161 | /// 162 | /// Gets the minimum version clients have to be on in order to connect to this server. 163 | /// 164 | [JsonIgnore] 165 | public Version MinimumVersion { get { return new Version(MinimumVersionString); } } 166 | 167 | /// 168 | /// Gets or sets whether the server will automatically create a new account whenever a user 169 | /// attempts to log in with an account that does not exist. 170 | /// 171 | public bool AutoCreateAccounts { get; set; } 172 | 173 | /// 174 | /// Gets or sets the port for the API server. 175 | /// 176 | public ushort Port { get; set; } 177 | 178 | /// 179 | /// Gets or sets the port for the XMPP server. 180 | /// 181 | public ushort XmppPort { get; set; } 182 | 183 | /// 184 | /// Gets or sets the port for the matchmaker. 185 | /// 186 | public ushort MatchmakerPort { get; set; } 187 | 188 | /// 189 | /// Gets or sets the list of cosmetics, quests, and other items to give to new accounts. 190 | /// 191 | public HashSet DefaultAthenaItems { get; set; } 192 | 193 | /// 194 | /// Gets or sets the list of banners and colors to give to new accounts. 195 | /// 196 | public HashSet DefaultCoreItems { get; set; } 197 | 198 | /// 199 | /// Gets or sets the list of currently equipped items that will be on an account. 200 | /// 201 | public Dictionary EquippedItems { get; set; } 202 | 203 | /// 204 | /// Gets or sets whether to log all valid HTTP requests. 205 | /// 206 | public bool LogHttpRequests { get; set; } 207 | 208 | /// 209 | /// Gets or sets whether to log all HTTP requests to nonexistent endpoints. This setting is 210 | /// independent from . 211 | /// 212 | public bool Log404 { get; set; } 213 | 214 | /// 215 | /// Gets or sets the list of events. 216 | /// 217 | public List ClientEvents { get; set; } 218 | } 219 | } -------------------------------------------------------------------------------- /ModniteServer/Controls/LogStreamEntry.xaml: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 80 | 146 | 147 | 148 | 149 | 150 | 151 | -------------------------------------------------------------------------------- /ModniteServer.Api/Controllers/ProfileController.cs: -------------------------------------------------------------------------------- 1 | using ModniteServer.API.Accounts; 2 | using Newtonsoft.Json; 3 | using Serilog; 4 | using System; 5 | using System.Collections.Generic; 6 | 7 | namespace ModniteServer.API.Controllers 8 | { 9 | public sealed class ProfileController : Controller 10 | { 11 | private string _accountId; 12 | private Account _account; 13 | private int _revision; 14 | 15 | [Route("POST", "/fortnite/api/game/v2/profile/*/client/QueryProfile")] 16 | public void QueryProfile() 17 | { 18 | _accountId = Request.Url.Segments[Request.Url.Segments.Length - 3].Replace("/", ""); 19 | 20 | if (!AccountManager.AccountExists(_accountId)) 21 | { 22 | Response.StatusCode = 404; 23 | return; 24 | } 25 | 26 | _account = AccountManager.GetAccount(_accountId); 27 | 28 | Query.TryGetValue("profileId", out string profileId); 29 | Query.TryGetValue("rvn", out string rvn); 30 | _revision = Convert.ToInt32(rvn ?? "-2"); 31 | 32 | switch (profileId) 33 | { 34 | case "common_core": 35 | QueryCommonCoreProfile(); 36 | break; 37 | 38 | case "common_public": 39 | QueryCommonPublicProfile(); 40 | break; 41 | 42 | case "athena": 43 | QueryAthenaProfile(); 44 | break; 45 | 46 | default: 47 | Response.StatusCode = 500; 48 | break; 49 | } 50 | } 51 | 52 | private void QueryCommonCoreProfile() 53 | { 54 | var items = _account.CoreItems; 55 | 56 | var itemsFormatted = new Dictionary(); 57 | foreach (string item in items) 58 | { 59 | itemsFormatted.Add(Guid.NewGuid().ToString(), new 60 | { 61 | templateId = item, 62 | attributes = new 63 | { 64 | item_seen = 1 65 | }, 66 | quantity = 1 67 | }); 68 | } 69 | 70 | var response = new 71 | { 72 | profileRevision = 8, 73 | profileId = "common_core", 74 | profileChangesBaseRevision = 8, 75 | profileChanges = new List 76 | { 77 | new 78 | { 79 | changeType = "fullProfileUpdate", 80 | profile = new 81 | { 82 | _id = _accountId, // not really account id but idk 83 | created = DateTime.Now.AddDays(-7).ToDateTimeString(), 84 | updated = DateTime.Now.AddDays(-1).ToDateTimeString(), 85 | rvn = 8, 86 | wipeNumber = 9, 87 | accountId = _accountId, 88 | profileId = "common_core", 89 | version = "grant_skirmish_banners_october_2018", 90 | items = itemsFormatted, 91 | stats = new 92 | { 93 | attributes = new 94 | { 95 | mtx_grace_balance = 0, 96 | import_friends_claimed = new List(), 97 | mtx_purchase_history = new List(), 98 | inventory_limit_bonus = 0, 99 | current_mtx_platform = "EpicPC", 100 | mtx_affiliate = "", 101 | weekly_purchases = new List(), 102 | daily_purchases = new List(), 103 | ban_history = new List(), 104 | in_app_purchases = new List(), 105 | monthly_purchases = new List(), 106 | allowed_to_send_gifts = false, 107 | mfa_enabled = false, 108 | allowed_to_receive_gifts = false, 109 | gift_history = new List() 110 | } 111 | }, 112 | commandRevision = 4 113 | } 114 | } 115 | }, 116 | profileCommandRevision = 4, 117 | serverTime = DateTime.Now.ToDateTimeString(), 118 | responseVersion = 1 119 | }; 120 | 121 | Log.Information("Retrieved profile 'common_core' {AccountId}{Profile}{Revision}", _accountId, response, _revision); 122 | 123 | Response.StatusCode = 200; 124 | Response.ContentType = "application/json"; 125 | Response.Write(JsonConvert.SerializeObject(response)); 126 | } 127 | 128 | private void QueryCommonPublicProfile() 129 | { 130 | var response = new 131 | { 132 | profileRevision = 1, 133 | profileId = "common_public", 134 | profileChangesBaseRevision = 1, 135 | profileChanges = new List(), 136 | profileCommandRevision = 0, 137 | serverTime = DateTime.Now.ToDateTimeString(), 138 | responseVersion = 1 139 | }; 140 | 141 | Log.Information("Retrieved profile 'common_public' {AccountId}{Profile}{Revision}", _accountId, response, _revision); 142 | 143 | Response.StatusCode = 200; 144 | Response.ContentType = "application/json"; 145 | Response.Write(JsonConvert.SerializeObject(response)); 146 | } 147 | 148 | private void QueryAthenaProfile() 149 | { 150 | // NOTE: Athena items are actually given in ClientQuestController. 151 | 152 | var items = _account.AthenaItems; 153 | var itemsFormatted = new Dictionary(); 154 | foreach (string item in items) 155 | { 156 | var itemGuid = item; // Makes life easier - config file doesn't store numbers anymore, and it can be read anywhere. 157 | itemsFormatted.Add(itemGuid, new 158 | { 159 | templateId = item, 160 | attributes = new 161 | { 162 | max_level_bonus = 0, 163 | level = 1, 164 | item_seen = 1, 165 | xp = 0, 166 | variants = new List(), 167 | favorite = false 168 | }, 169 | quantity = 1 170 | }); 171 | } 172 | 173 | string[] dances = new string[6]; 174 | for (int i = 0; i < dances.Length; i++) 175 | { 176 | dances[i] = _account.EquippedItems["favorite_dance" + i]; 177 | } 178 | 179 | var response = new 180 | { 181 | profileRevision = 10, 182 | profileId = "athena", 183 | profileChangesBaseRevision = 10, 184 | profileChanges = new List 185 | { 186 | new 187 | { 188 | changeType = "fullProfileUpdate", 189 | profile = new 190 | { 191 | _id = _accountId, 192 | created = DateTime.Now.AddDays(-7).ToDateTimeString(), 193 | updated = DateTime.Now.AddDays(-1).ToDateTimeString(), 194 | rvn = 10, 195 | wipeNumber = 5, 196 | accountId = _accountId, 197 | profileId = "athena", 198 | version = "fortnitemares_part4_fixup_oct_18", 199 | items = itemsFormatted, 200 | stats = new 201 | { 202 | attributes = new 203 | { 204 | past_seasons = new string[0], 205 | season_match_boost = 1000, 206 | favorite_victorypose = "", 207 | mfa_reward_claimed = false, 208 | quest_manager = new 209 | { 210 | dailyLoginInterval = DateTime.Now.AddDays(1).ToDateTimeString(), 211 | dailyQuestRerolls = 1 212 | }, 213 | book_level = 0, 214 | season_num = 0, 215 | favorite_consumableemote = "", 216 | banner_color = "defaultcolor1", 217 | favorite_callingcard = "", 218 | favorite_character = _account.EquippedItems["favorite_character"], 219 | favorite_spray = new string[0], 220 | book_xp = 0, 221 | favorite_loadingscreen = _account.EquippedItems["favorite_loadingscreen"], 222 | book_purchased = false, 223 | lifetime_wins = 0, 224 | favorite_hat = "", 225 | level = 1000000, 226 | favorite_battlebus = "", 227 | favorite_mapmarker = "", 228 | favorite_vehicledeco = "", 229 | accountLevel = 1000000, 230 | favorite_backpack = _account.EquippedItems["favorite_backpack"], 231 | favorite_dance = dances, 232 | inventory_limit_bonus = 0, 233 | favorite_skydivecontrail = _account.EquippedItems["favorite_skydivecontrail"], 234 | favorite_pickaxe = _account.EquippedItems["favorite_pickaxe"], 235 | favorite_glider = _account.EquippedItems["favorite_glider"], 236 | daily_rewards = new { }, 237 | xp = 0, 238 | season_friend_match_boost = 0, 239 | favorite_musicpack = _account.EquippedItems["favorite_musicpack"], 240 | banner_icon = "standardbanner1" 241 | } 242 | }, 243 | commandRevision = 5 244 | } 245 | } 246 | }, 247 | profileCommandRevision = 5, 248 | serverTime = DateTime.Now.ToDateTimeString(), 249 | responseVersion = 1 250 | }; 251 | 252 | Log.Information("Retrieved profile 'athena' {AccountId}{Profile}{Revision}", _accountId, response, _revision); 253 | 254 | Response.StatusCode = 200; 255 | Response.ContentType = "application/json"; 256 | Response.Write(JsonConvert.SerializeObject(response)); 257 | } 258 | } 259 | } 260 | --------------------------------------------------------------------------------