diff --git a/BackendServices/MultiServerLibrary/HTTP/HTTPProcessor.cs b/BackendServices/MultiServerLibrary/HTTP/HTTPProcessor.cs index 84278e85a..3fc2e7649 100644 --- a/BackendServices/MultiServerLibrary/HTTP/HTTPProcessor.cs +++ b/BackendServices/MultiServerLibrary/HTTP/HTTPProcessor.cs @@ -802,6 +802,13 @@ public static string DecodeUrl(string url) return newUrl; } + public static Uri ParseUriFromAbsolutePath(string input) + { + const string fakeDomain = "http://test.com"; + + return new Uri(fakeDomain + input); + } + // HTTP requires that responses contain the proper MIME type. This quick mapping list below // contains many more mimetypes than System.Web.MimeMapping diff --git a/Servers/SSFWServer/SSFWDataMigrator.cs b/Servers/SSFWServer/Helpers/DataMigrator/DataMigrator.cs similarity index 97% rename from Servers/SSFWServer/SSFWDataMigrator.cs rename to Servers/SSFWServer/Helpers/DataMigrator/DataMigrator.cs index dcc2d1db5..1f0d828c0 100644 --- a/Servers/SSFWServer/SSFWDataMigrator.cs +++ b/Servers/SSFWServer/Helpers/DataMigrator/DataMigrator.cs @@ -1,8 +1,8 @@ using MultiServerLibrary.Extension; -namespace SSFWServer +namespace SSFWServer.Helpers.DataMigrator { - public class SSFWDataMigrator + public class DataMigrator { public static void MigrateSSFWData(string ssfwrootDirectory, string oldStr, string? newStr) { diff --git a/Servers/SSFWServer/ScenelistParser.cs b/Servers/SSFWServer/Helpers/FileHelper/ScenelistParser.cs similarity index 98% rename from Servers/SSFWServer/ScenelistParser.cs rename to Servers/SSFWServer/Helpers/FileHelper/ScenelistParser.cs index de84a11e5..6ea05b95c 100644 --- a/Servers/SSFWServer/ScenelistParser.cs +++ b/Servers/SSFWServer/Helpers/FileHelper/ScenelistParser.cs @@ -2,7 +2,7 @@ using System.Collections.Concurrent; using System.Xml; -namespace SSFWServer +namespace SSFWServer.Helpers.FileHelper { public class ScenelistParser { @@ -34,7 +34,8 @@ public static void UpdateSceneDictionary(object? state) if (sceneNodes != null) { // Extract ChannelID and SCENE ID from each SCENE element - Parallel.ForEach(sceneNodes.Cast(), sceneNode => { + Parallel.ForEach(sceneNodes.Cast(), sceneNode => + { if (sceneNode.Attributes != null) { string? ID = sceneNode.Attributes["ID"]?.Value; diff --git a/Servers/SSFWServer/Helpers/RegexHelper/GUIDValidator.cs b/Servers/SSFWServer/Helpers/RegexHelper/GUIDValidator.cs index b1d89be72..3cf11d14d 100644 --- a/Servers/SSFWServer/Helpers/RegexHelper/GUIDValidator.cs +++ b/Servers/SSFWServer/Helpers/RegexHelper/GUIDValidator.cs @@ -6,6 +6,24 @@ namespace SSFWServer.Helpers.RegexHelper { public class GUIDValidator { + public static Regex RegexSessionValidator = new(@"^[{(]?([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})[)}]?$"); + public static Regex VersionFilter = new(@"\d{6}"); + +#if NET7_0_OR_GREATER + [GeneratedRegex("[0-9a-fA-F]{8}-[0-9a-fA-F]{8}-[0-9a-fA-F]{8}-[0-9a-fA-F]{8}")] + private static partial Regex UUIDRegex(); +#endif + + public static Match RegexSceneIdValidMatch(string sceneId) + { +#if NET7_0_OR_GREATER + Match match = UUIDRegex().Match(sceneid); +#else + Match match = new Regex(@"[0-9a-fA-F]{8}-[0-9a-fA-F]{8}-[0-9a-fA-F]{8}-[0-9a-fA-F]{8}").Match(sceneId); +#endif + return match; + } + public static string FixJsonValues(string json) { // Match GUID portion with 8-8-8-8 format (fix unquoted GUIDs) diff --git a/Servers/SSFWServer/Program.cs b/Servers/SSFWServer/Program.cs index f9108a641..bb6ead680 100644 --- a/Servers/SSFWServer/Program.cs +++ b/Servers/SSFWServer/Program.cs @@ -1,144 +1,17 @@ using CustomLogger; -using Newtonsoft.Json.Linq; using SSFWServer; using System.Reflection; using System.Runtime; -using System.Security.Cryptography; -using MultiServerLibrary.SNMP; -using MultiServerLibrary; using Microsoft.Extensions.Logging; -using MultiServerLibrary.Extension; - -public static class SSFWServerConfiguration -{ - public static bool SSFWCrossSave { get; set; } = true; - public static bool EnableHTTPCompression { get; set; } = false; - public static int SSFWTTL { get; set; } = 180; - public static string SSFWMinibase { get; set; } = "[]"; - public static string SSFWLegacyKey { get; set; } = "**NoNoNoYouCantHaxThis****69"; - public static string SSFWSessionIdKey { get; set; } = StringUtils.GenerateRandomBase64KeyAsync().Result; - public static string SSFWStaticFolder { get; set; } = $"{Directory.GetCurrentDirectory()}/static/wwwssfwroot"; - public static string HTTPSCertificateFile { get; set; } = $"{Directory.GetCurrentDirectory()}/static/SSL/SSFW.pfx"; - public static string HTTPSCertificatePassword { get; set; } = "qwerty"; - public static HashAlgorithmName HTTPSCertificateHashingAlgorithm { get; set; } = HashAlgorithmName.SHA384; - public static string ScenelistFile { get; set; } = $"{Directory.GetCurrentDirectory()}/static/wwwssfwroot/SceneList.xml"; - public static string[]? HTTPSDNSList { get; set; } = { - "cprod.homerewards.online.scee.com", - "cprod.homeidentity.online.scee.com", - "cprod.homeserverservices.online.scee.com", - "cdev.homerewards.online.scee.com", - "cdev.homeidentity.online.scee.com", - "cdev.homeserverservices.online.scee.com", - "cdevb.homerewards.online.scee.com", - "cdevb.homeidentity.online.scee.com", - "cdevb.homeserverservices.online.scee.com", - "nonprod1.homerewards.online.scee.com", - "nonprod1.homeidentity.online.scee.com", - "nonprod1.homeserverservices.online.scee.com", - "nonprod2.homerewards.online.scee.com", - "nonprod2.homeidentity.online.scee.com", - "nonprod2.homeserverservices.online.scee.com", - "nonprod3.homerewards.online.scee.com", - "nonprod3.homeidentity.online.scee.com", - "nonprod3.homeserverservices.online.scee.com", - "nonprod4.homerewards.online.scee.com", - "nonprod4.homeidentity.online.scee.com", - "nonprod4.homeserverservices.online.scee.com", - }; - - /// - /// Tries to load the specified configuration file. - /// Throws an exception if it fails to find the file. - /// - /// - /// - public static void RefreshVariables(string configPath) - { - // Make sure the file exists - if (!File.Exists(configPath)) - { - LoggerAccessor.LogWarn($"Could not find the configuration file:{configPath}, writing and using server's default."); - - Directory.CreateDirectory(Path.GetDirectoryName(configPath) ?? Directory.GetCurrentDirectory() + "/static"); - - // Write the JObject to a file - File.WriteAllText(configPath, new JObject( - new JProperty("config_version", (ushort)2), - new JProperty("minibase", SSFWMinibase), - new JProperty("legacyKey", SSFWLegacyKey), - new JProperty("sessionidKey", SSFWSessionIdKey), - new JProperty("time_to_live", SSFWTTL), - new JProperty("cross_save", SSFWCrossSave), - new JProperty("enable_http_compression", EnableHTTPCompression), - new JProperty("static_folder", SSFWStaticFolder), - new JProperty("https_dns_list", HTTPSDNSList ?? Array.Empty()), - new JProperty("certificate_file", HTTPSCertificateFile), - new JProperty("certificate_password", HTTPSCertificatePassword), - new JProperty("certificate_hashing_algorithm", HTTPSCertificateHashingAlgorithm.Name), - new JProperty("scenelist_file", ScenelistFile) - ).ToString()); - - return; - } - - try - { - // Parse the JSON configuration - dynamic config = JObject.Parse(File.ReadAllText(configPath)); - - ushort config_version = GetValueOrDefault(config, "config_version", (ushort)0); - if (config_version >= 2) - EnableHTTPCompression = GetValueOrDefault(config, "enable_http_compression", EnableHTTPCompression); - SSFWMinibase = GetValueOrDefault(config, "minibase", SSFWMinibase); - SSFWTTL = GetValueOrDefault(config, "time_to_live", SSFWTTL); - SSFWLegacyKey = GetValueOrDefault(config, "legacyKey", SSFWLegacyKey); - SSFWSessionIdKey = GetValueOrDefault(config, "sessionidKey", SSFWSessionIdKey); - SSFWCrossSave = GetValueOrDefault(config, "cross_save", SSFWCrossSave); - SSFWStaticFolder = GetValueOrDefault(config, "static_folder", SSFWStaticFolder); - HTTPSCertificateFile = GetValueOrDefault(config, "certificate_file", HTTPSCertificateFile); - HTTPSCertificatePassword = GetValueOrDefault(config, "certificate_password", HTTPSCertificatePassword); - HTTPSCertificateHashingAlgorithm = new HashAlgorithmName(GetValueOrDefault(config, "certificate_hashing_algorithm", HTTPSCertificateHashingAlgorithm.Name)); - HTTPSDNSList = GetValueOrDefault(config, "https_dns_list", HTTPSDNSList); - ScenelistFile = GetValueOrDefault(config, "scenelist_file", ScenelistFile); - } - catch (Exception ex) - { - LoggerAccessor.LogWarn($"{configPath} file is malformed (exception: {ex}), using server's default."); - } - } - - // Helper method to get a value or default value if not present - private static T GetValueOrDefault(dynamic obj, string propertyName, T defaultValue) - { - try - { - if (obj != null) - { - if (obj is JObject jObject) - { - if (jObject.TryGetValue(propertyName, out JToken? value)) - { - T? returnvalue = value.ToObject(); - if (returnvalue != null) - return returnvalue; - } - } - } - } - catch (Exception ex) - { - LoggerAccessor.LogError($"[Program] - GetValueOrDefault thrown an exception: {ex}"); - } - - return defaultValue; - } -} +using MultiServerLibrary; +using MultiServerLibrary.SNMP; +using SSFWServer.Helpers.FileHelper; class Program { - private static string configDir = Directory.GetCurrentDirectory() + "/static/"; - private static string configPath = configDir + "SSFWServer.json"; - private static string configMultiServerLibraryPath = configDir + "MultiServerLibrary.json"; + private static readonly string configDir = Directory.GetCurrentDirectory() + "/static/"; + private static readonly string configPath = configDir + "SSFWServer.json"; + private static readonly string configMultiServerLibraryPath = configDir + "MultiServerLibrary.json"; private static SnmpTrapSender? trapSender = null; private static Timer? SceneListTimer; private static Timer? SessionTimer; @@ -290,4 +163,20 @@ static void Main() Thread.Sleep(Timeout.Infinite); } } + + /// + /// Extract a portion of a string winthin boundaries. + /// Extrait une portion d'un string entre des limites. + /// + /// The input string. + /// The amount of characters to remove from the left to the right. + /// The amount of characters to remove from the right to the left. + /// A string. + public static string? ExtractPortion(string input, int startToRemove, int endToRemove) + { + if (input.Length < startToRemove + endToRemove) + return null; + + return input[startToRemove..][..^endToRemove]; + } } \ No newline at end of file diff --git a/Servers/SSFWServer/SSFWAccountManagement.cs b/Servers/SSFWServer/SSFWAccountManagement.cs index 05a711e5c..957299b07 100644 --- a/Servers/SSFWServer/SSFWAccountManagement.cs +++ b/Servers/SSFWServer/SSFWAccountManagement.cs @@ -5,6 +5,13 @@ namespace SSFWServer { + public class SSFWUserData + { + public string? Username { get; set; } + public int LogonCount { get; set; } + public int IGA { get; set; } + } + public class SSFWAccountManagement { public static int ReadOrMigrateAccount(byte[] extractedData, string? username, string? sessionid, string? key) diff --git a/Servers/SSFWServer/SSFWMisc.cs b/Servers/SSFWServer/SSFWMisc.cs deleted file mode 100644 index c26a0ed73..000000000 --- a/Servers/SSFWServer/SSFWMisc.cs +++ /dev/null @@ -1,90 +0,0 @@ -namespace SSFWServer -{ - public class SSFWMisc - { - public static readonly string LegacyLayoutTemplate = "[{\"00000000-00000000-00000000-00000004\":{\"version\":3,\"wallpaper\":2,\"furniture\":" + - "[{\"flags\":0,\"furnitureObjectId\":\"00000000-00000000-00000002-00000010\",\"instanceId\":\"4874595585\",\"itemId\":0,\"" + - "positionX\":-4.287144660949707,\"positionY\":2.9999580383300781,\"positionZ\":-2.3795166015625,\"rotationX\":2.6903744583" + - "250955E-06,\"rotationY\":0.70767402648925781,\"rotationZ\":-2.1571504476014525E-06,\"rotationW\":0.70653915405273438,\"ti" + - "me\":1686384673},{\"flags\":0,\"furnitureObjectId\":\"00000000-00000000-00000002-00000002\",\"instanceId\":\"4874595586\"" + - ",\"itemId\":1,\"positionX\":-3.7360246181488037,\"positionY\":2.9999902248382568,\"positionZ\":-0.93418246507644653,\"rot" + - "ationX\":1.5251726836140733E-05,\"rotationY\":0.92014747858047485,\"rotationZ\":-0.00032892703893594444,\"rotationW\":0.3" + - "9157184958457947,\"time\":1686384699},{\"flags\":0,\"furnitureObjectId\":\"00000000-00000000-00000002-00000002\",\"instan" + - "ceId\":\"4874595587\",\"itemId\":2,\"positionX\":-4.2762022018432617,\"positionY\":2.9999568462371826,\"positionZ\":-4.15" + - "23990631103516,\"rotationX\":1.4554960570123399E-09,\"rotationY\":0.4747755229473114,\"rotationZ\":-1.4769816480963982E-0" + - "8,\"rotationW\":0.88010692596435547,\"time\":1686384723},{\"flags\":0,\"furnitureObjectId\":\"00000000-00000000-00000002-" + - "00000002\",\"instanceId\":\"4874595588\",\"itemId\":3,\"positionX\":-2.8646721839904785,\"positionY\":2.9999570846557617," + - "\"positionZ\":-3.0560495853424072,\"rotationX\":0.00010053320875158533,\"rotationY\":-0.26336261630058289,\"rotationZ\":-" + - "3.8589099858654663E-05,\"rotationW\":0.96469688415527344,\"time\":1686384751},{\"flags\":0,\"furnitureObjectId\":\"000000" + - "00-00000000-00000002-00000001\",\"instanceId\":\"4874595589\",\"itemId\":4,\"positionX\":3.9096813201904297,\"positionY\"" + - ":2.9995136260986328,\"positionZ\":-4.2813630104064941,\"rotationX\":4.3287433072691783E-05,\"rotationY\":-0.5309971570968" + - "6279,\"rotationZ\":-3.9187150832731277E-05,\"rotationW\":0.8473736047744751,\"time\":1686384774},{\"flags\":0,\"furniture" + - "ObjectId\":\"00000000-00000000-00000002-00000004\",\"instanceId\":\"4874595590\",\"itemId\":5,\"positionX\":1.84187448024" + - "74976,\"positionY\":3.0001647472381592,\"positionZ\":-3.2746503353118896,\"rotationX\":-5.4990476201055571E-05,\"rotation" + - "Y\":-0.53177982568740845,\"rotationZ\":-1.335094293608563E-05,\"rotationW\":0.84688264131546021,\"time\":1686384795},{\"f" + - "lags\":0,\"furnitureObjectId\":\"00000000-00000000-00000002-00000008\",\"instanceId\":\"4874595591\",\"itemId\":6,\"posit" + - "ionX\":3.4726400375366211,\"positionY\":3.0000433921813965,\"positionZ\":4.783566951751709,\"rotationX\":6.13473239354789" + - "26E-05,\"rotationY\":0.99999260902404785,\"rotationZ\":-1.7070769899873994E-05,\"rotationW\":0.0038405421655625105,\"time" + - "\":1686384822},{\"flags\":0,\"furnitureObjectId\":\"00000000-00000000-00000002-00000008\",\"instanceId\":\"4874595592\"," + - "\"itemId\":7,\"positionX\":3.4952659606933594,\"positionY\":3.0000007152557373,\"positionZ\":0.2776024341583252,\"rotatio" + - "nX\":-1.2929040167364292E-05,\"rotationY\":-0.0061355167999863625,\"rotationZ\":-4.4378830352798104E-05,\"rotationW\":0.9" + - "99981164932251,\"time\":1686384834},{\"flags\":0,\"furnitureObjectId\":\"00000000-00000000-00000002-00000001\",\"instance" + - "Id\":\"4874595593\",\"itemId\":8,\"positionX\":1.3067165613174438,\"positionY\":2.9994897842407227,\"positionZ\":2.546649" + - "694442749,\"rotationX\":2.8451957405195571E-05,\"rotationY\":0.70562022924423218,\"rotationZ\":-8.0827621786738746E-06,\"" + - "rotationW\":0.70859026908874512,\"time\":1686384862},{\"flags\":0,\"furnitureObjectId\":\"00000000-00000000-00000002-0000" + - "0003\",\"instanceId\":\"4874595594\",\"itemId\":9,\"positionX\":3.4803681373596191,\"positionY\":2.9999568462371826,\"pos" + - "itionZ\":2.5385856628417969,\"rotationX\":3.1659130428352E-08,\"rotationY\":-0.70712763071060181,\"rotationZ\":8.14428204" + - "87609424E-08,\"rotationW\":0.70708584785461426,\"time\":1686384884},{\"flags\":0,\"furnitureObjectId\":\"00000000-0000000" + - "0-00000002-00000009\",\"instanceId\":\"4874595595\",\"itemId\":10,\"positionX\":-3.5043892860412598,\"positionY\":2.99995" + - "68462371826,\"positionZ\":-9.527653694152832,\"rotationX\":-1.7184934222314041E-06,\"rotationY\":0.00023035785125102848,\"" + - "rotationZ\":2.5227839728358958E-07,\"rotationW\":0.99999994039535522,\"time\":1686384912},{\"flags\":0,\"furnitureObjectI" + - "d\":\"00000000-00000000-00000002-00000009\",\"instanceId\":\"4874595596\",\"itemId\":11,\"positionX\":3.6248698234558105," + - "\"positionY\":2.9999566078186035,\"positionZ\":-9.5347089767456055,\"rotationX\":-2.1324558474589139E-07,\"rotationY\":2." + - "0361580027383752E-05,\"rotationZ\":-4.7822368287597783E-08,\"rotationW\":1,\"time\":1686384931},{\"flags\":0,\"furnitureO" + - "bjectId\":\"00000000-00000000-00000002-00000005\",\"instanceId\":\"4874595597\",\"itemId\":12,\"positionX\":-3.5068926811" + - "218262,\"positionY\":3.4883472919464111,\"positionZ\":-9.5313901901245117,\"rotationX\":-0.00091801158851012588,\"rotatio" + - "nY\":0.006055513396859169,\"rotationZ\":0.000585820700507611,\"rotationW\":0.99998104572296143,\"time\":1686384961,\"phot" + - "o\":\"/Furniture/Modern2/lampOutputcube.dds\"},{\"flags\":0,\"furnitureObjectId\":\"00000000-00000000-00000002-00000005\"" + - ",\"instanceId\":\"4874595598\",\"itemId\":13,\"positionX\":3.6171293258666992,\"positionY\":3.4891724586486816,\"position" + - "Z\":-9.53490161895752,\"rotationX\":0.00042979296995326877,\"rotationY\":-0.0092521701008081436,\"rotationZ\":-0.00027207" + - "753737457097,\"rotationW\":0.99995702505111694,\"time\":1686385008,\"photo\":\"/Furniture/Modern2/lampOutputcube.dds\"}]}}]"; - - public static readonly string HarbourStudioLayout = "{\r\n \"version\": 3,\r\n \"wallpaper\": 2,\r\n \"furniture\": [\r\n {\r\n \"flags\": 0,\r\n \"furnitureObjectId\": \"00000000-00000000-00000002-00000010\",\r\n \"instanceId\": \"4874595585\",\r\n \"itemId\": 0,\r\n \"positionX\": -4.287144660949707,\r\n \"positionY\": 2.999958038330078,\r\n \"positionZ\": -2.3795166015625,\r\n \"rotationX\": 2.6903744583250955E-06,\r\n \"rotationY\": 0.7076740264892578,\r\n \"rotationZ\": -2.1571504476014525E-06,\r\n \"rotationW\": 0.7065391540527344,\r\n \"time\": 1686384673\r\n },\r\n {\r\n \"flags\": 0,\r\n \"furnitureObjectId\": \"00000000-00000000-00000002-00000002\",\r\n \"instanceId\": \"4874595586\",\r\n \"itemId\": 1,\r\n \"positionX\": -3.7360246181488037,\r\n \"positionY\": 2.999990224838257,\r\n \"positionZ\": -0.9341824650764465,\r\n \"rotationX\": 1.5251726836140733E-05,\r\n \"rotationY\": 0.9201474785804749,\r\n \"rotationZ\": -0.00032892703893594444,\r\n \"rotationW\": 0.39157184958457947,\r\n \"time\": 1686384699\r\n },\r\n {\r\n \"flags\": 0,\r\n \"furnitureObjectId\": \"00000000-00000000-00000002-00000002\",\r\n \"instanceId\": \"4874595587\",\r\n \"itemId\": 2,\r\n \"positionX\": -4.276202201843262,\r\n \"positionY\": 2.9999568462371826,\r\n \"positionZ\": -4.152399063110352,\r\n \"rotationX\": 1.4554960570123399E-09,\r\n \"rotationY\": 0.4747755229473114,\r\n \"rotationZ\": -1.4769816480963982E-08,\r\n \"rotationW\": 0.8801069259643555,\r\n \"time\": 1686384723\r\n },\r\n {\r\n \"flags\": 0,\r\n \"furnitureObjectId\": \"00000000-00000000-00000002-00000002\",\r\n \"instanceId\": \"4874595588\",\r\n \"itemId\": 3,\r\n \"positionX\": -2.8646721839904785,\r\n \"positionY\": 2.9999570846557617,\r\n \"positionZ\": -3.0560495853424072,\r\n \"rotationX\": 0.00010053320875158533,\r\n \"rotationY\": -0.2633626163005829,\r\n \"rotationZ\": -3.858909985865466E-05,\r\n \"rotationW\": 0.9646968841552734,\r\n \"time\": 1686384751\r\n },\r\n {\r\n \"flags\": 0,\r\n \"furnitureObjectId\": \"00000000-00000000-00000002-00000001\",\r\n \"instanceId\": \"4874595589\",\r\n \"itemId\": 4,\r\n \"positionX\": 3.9096813201904297,\r\n \"positionY\": 2.999513626098633,\r\n \"positionZ\": -4.281363010406494,\r\n \"rotationX\": 4.328743307269178E-05,\r\n \"rotationY\": -0.5309971570968628,\r\n \"rotationZ\": -3.918715083273128E-05,\r\n \"rotationW\": 0.8473736047744751,\r\n \"time\": 1686384774\r\n },\r\n {\r\n \"flags\": 0,\r\n \"furnitureObjectId\": \"00000000-00000000-00000002-00000004\",\r\n \"instanceId\": \"4874595590\",\r\n \"itemId\": 5,\r\n \"positionX\": 1.8418744802474976,\r\n \"positionY\": 3.000164747238159,\r\n \"positionZ\": -3.2746503353118896,\r\n \"rotationX\": -5.499047620105557E-05,\r\n \"rotationY\": -0.5317798256874084,\r\n \"rotationZ\": -1.335094293608563E-05,\r\n \"rotationW\": 0.8468826413154602,\r\n \"time\": 1686384795\r\n },\r\n {\r\n \"flags\": 0,\r\n \"furnitureObjectId\": \"00000000-00000000-00000002-00000008\",\r\n \"instanceId\": \"4874595591\",\r\n \"itemId\": 6,\r\n \"positionX\": 3.472640037536621,\r\n \"positionY\": 3.0000433921813965,\r\n \"positionZ\": 4.783566951751709,\r\n \"rotationX\": 6.134732393547893E-05,\r\n \"rotationY\": 0.9999926090240479,\r\n \"rotationZ\": -1.7070769899873994E-05,\r\n \"rotationW\": 0.0038405421655625105,\r\n \"time\": 1686384822\r\n },\r\n {\r\n \"flags\": 0,\r\n \"furnitureObjectId\": \"00000000-00000000-00000002-00000008\",\r\n \"instanceId\": \"4874595592\",\r\n \"itemId\": 7,\r\n \"positionX\": 3.4952659606933594,\r\n \"positionY\": 3.0000007152557373,\r\n \"positionZ\": 0.2776024341583252,\r\n \"rotationX\": -1.2929040167364292E-05,\r\n \"rotationY\": -0.0061355167999863625,\r\n \"rotationZ\": -4.4378830352798104E-05,\r\n \"rotationW\": 0.999981164932251,\r\n \"time\": 1686384834\r\n },\r\n {\r\n \"flags\": 0,\r\n \"furnitureObjectId\": \"00000000-00000000-00000002-00000001\",\r\n \"instanceId\": \"4874595593\",\r\n \"itemId\": 8,\r\n \"positionX\": 1.3067165613174438,\r\n \"positionY\": 2.9994897842407227,\r\n \"positionZ\": 2.546649694442749,\r\n \"rotationX\": 2.845195740519557E-05,\r\n \"rotationY\": 0.7056202292442322,\r\n \"rotationZ\": -8.082762178673875E-06,\r\n \"rotationW\": 0.7085902690887451,\r\n \"time\": 1686384862\r\n },\r\n {\r\n \"flags\": 0,\r\n \"furnitureObjectId\": \"00000000-00000000-00000002-00000003\",\r\n \"instanceId\": \"4874595594\",\r\n \"itemId\": 9,\r\n \"positionX\": 3.480368137359619,\r\n \"positionY\": 2.9999568462371826,\r\n \"positionZ\": 2.538585662841797,\r\n \"rotationX\": 3.1659130428352E-08,\r\n \"rotationY\": -0.7071276307106018,\r\n \"rotationZ\": 8.144282048760942E-08,\r\n \"rotationW\": 0.7070858478546143,\r\n \"time\": 1686384884\r\n },\r\n {\r\n \"flags\": 0,\r\n \"furnitureObjectId\": \"00000000-00000000-00000002-00000009\",\r\n \"instanceId\": \"4874595595\",\r\n \"itemId\": 10,\r\n \"positionX\": -3.5043892860412598,\r\n \"positionY\": 2.9999568462371826,\r\n \"positionZ\": -9.527653694152832,\r\n \"rotationX\": -1.7184934222314041E-06,\r\n \"rotationY\": 0.00023035785125102848,\r\n \"rotationZ\": 2.522783972835896E-07,\r\n \"rotationW\": 0.9999999403953552,\r\n \"time\": 1686384912\r\n },\r\n {\r\n \"flags\": 0,\r\n \"furnitureObjectId\": \"00000000-00000000-00000002-00000009\",\r\n \"instanceId\": \"4874595596\",\r\n \"itemId\": 11,\r\n \"positionX\": 3.6248698234558105,\r\n \"positionY\": 2.9999566078186035,\r\n \"positionZ\": -9.534708976745605,\r\n \"rotationX\": -2.132455847458914E-07,\r\n \"rotationY\": 2.0361580027383752E-05,\r\n \"rotationZ\": -4.782236828759778E-08,\r\n \"rotationW\": 1,\r\n \"time\": 1686384931\r\n },\r\n {\r\n \"flags\": 0,\r\n \"furnitureObjectId\": \"00000000-00000000-00000002-00000005\",\r\n \"instanceId\": \"4874595597\",\r\n \"itemId\": 12,\r\n \"positionX\": -3.506892681121826,\r\n \"positionY\": 3.488347291946411,\r\n \"positionZ\": -9.531390190124512,\r\n \"rotationX\": -0.0009180115885101259,\r\n \"rotationY\": 0.006055513396859169,\r\n \"rotationZ\": 0.000585820700507611,\r\n \"rotationW\": 0.9999810457229614,\r\n \"time\": 1686384961,\r\n \"photo\": \"/Furniture/Modern2/lampOutputcube.dds\"\r\n },\r\n {\r\n \"flags\": 0,\r\n \"furnitureObjectId\": \"00000000-00000000-00000002-00000005\",\r\n \"instanceId\": \"4874595598\",\r\n \"itemId\": 13,\r\n \"positionX\": 3.617129325866699,\r\n \"positionY\": 3.4891724586486816,\r\n \"positionZ\": -9.53490161895752,\r\n \"rotationX\": 0.00042979296995326877,\r\n \"rotationY\": -0.009252170100808144,\r\n \"rotationZ\": -0.00027207753737457097,\r\n \"rotationW\": 0.9999570250511169,\r\n \"time\": 1686385008,\r\n \"photo\": \"/Furniture/Modern2/lampOutputcube.dds\"\r\n }\r\n ]\r\n}"; - - // Sandbox Environments - public static List homeEnvs = new() - { - "cprod", "cprodts", "cpreprod", "cpreprodb", - "rc-qa", "rcdev", "rc-dev", "cqa-e", - "cqa-a", "cqa-j", "cqa-h", "cqab-e", - "cqab-a", "cqab-j", "cqab-h", "qcqa-e", - "qcqa-a", "qcqa-j", "qcqa-h", "qcpreprod", - "qcqab-e", "qcqab-a", "qcqab-j", "qcqab-h", - "qcpreprodb", "coredev", "core-dev", "core-qa", - "cdev", "cdev2", "cdev3", "cdeva", "cdevb", "cdevc" - }; - - /// - /// Extract a portion of a string winthin boundaries. - /// Extrait une portion d'un string entre des limites. - /// - /// The input string. - /// The amount of characters to remove from the left to the right. - /// The amount of characters to remove from the right to the left. - /// A string. - public static string? ExtractPortion(string input, int startToRemove, int endToRemove) - { - if (input.Length < startToRemove + endToRemove) - return null; - - return input[startToRemove..][..^endToRemove]; - } - } - - public class SSFWUserData - { - public string? Username { get; set; } - public int LogonCount { get; set; } - public int IGA { get; set; } - } -} diff --git a/Servers/SSFWServer/SSFWProcessor.cs b/Servers/SSFWServer/SSFWProcessor.cs index eb9fcdc26..a32af20d5 100644 --- a/Servers/SSFWServer/SSFWProcessor.cs +++ b/Servers/SSFWServer/SSFWProcessor.cs @@ -5,7 +5,6 @@ using NetCoreServer; using NetCoreServer.CustomServers; using SSFWServer.Helpers.FileHelper; -using SSFWServer.SaveDataHelper; using SSFWServer.Services; using System.Collections.Concurrent; using System.Net; @@ -15,7 +14,6 @@ using System.Security.Cryptography.X509Certificates; using System.Text.Json; using System.Text.RegularExpressions; -using Tpm2Lib; namespace SSFWServer { @@ -24,7 +22,7 @@ public class SSFWProcessor private const string LoginGUID = "bb88aea9-6bf8-4201-a6ff-5d1f8da0dd37"; // Defines a list of web-related file extensions - private static readonly HashSet allowedWebExtensions = new HashSet(StringComparer.InvariantCultureIgnoreCase) + private static readonly HashSet allowedWebExtensions = new(StringComparer.InvariantCultureIgnoreCase) { ".html", ".htm", ".cgi", ".css", ".js", ".svg", ".gif", ".ico", ".woff", ".woff2", ".ttf", ".eot" }; @@ -176,15 +174,22 @@ private static HttpResponse SSFWRequestProcess(HttpRequest request, HttpResponse string? env = ExtractBeforeFirstDot(host); sessionid = GetHeaderValue(Headers, "X-Home-Session-Id"); - if (string.IsNullOrEmpty(env) || !SSFWMisc.homeEnvs.Contains(env)) + if (string.IsNullOrEmpty(env) || !SSFWServerConfiguration.homeEnvs.Contains(env)) env = "cprod"; // Instantiate services - SSFWAuditService auditService = new(sessionid, env, _legacykey); - SSFWRewardsService rewardSvc = new(_legacykey); - SSFWLayoutService layout = new(_legacykey); - SSFWAvatarLayoutService avatarLayout = new(sessionid, _legacykey); - SSFWClanService clanService = new(sessionid); + AchievementService achievementService = new(sessionid, env, _legacykey); + AuditService auditService = new(sessionid, env, _legacykey); + AvatarService avatarService = new(); + FriendsService friendsService = new(sessionid, env, _legacykey); + KeepAliveService keepAliveService = new(); + RewardsService rewardSvc = new(_legacykey); + LayoutService layout = new(_legacykey); + AvatarLayoutService avatarLayout = new(sessionid, _legacykey); + ClanService clanService = new(sessionid); + PlayerLookupService playerLookupService = new(); + SaveDataService saveDataService = new(); + TradingService tradingService = new(sessionid, env, _legacykey); switch (request.Method) { @@ -216,6 +221,7 @@ private static HttpResponse SSFWRequestProcess(HttpRequest request, HttpResponse } else Response.MakeGetResponse(res, "application/json"); + break; } #endregion @@ -223,75 +229,116 @@ private static HttpResponse SSFWRequestProcess(HttpRequest request, HttpResponse else if (absolutepath.Contains("/AdminObjectService/start")) { Response.Clear(); - if (new SSFWAdminObjectService(sessionid, _legacykey).HandleAdminObjectService(UserAgent)) + if (new AdminObjectService(sessionid, _legacykey).HandleAdminObjectService(UserAgent)) Response.SetBegin((int)HttpStatusCode.OK); else Response.SetBegin((int)HttpStatusCode.Forbidden); Response.SetBody(); + break; } #endregion #region SaveDataService else if (absolutepath.Contains($"/SaveDataService/{env}/{segments.LastOrDefault()}")) { - string? res = SSFWGetFileList.SSFWSaveDataDebugGetFileList(directoryPath, segments.LastOrDefault()); + string? res = saveDataService.DebugGetFileList(directoryPath, segments.LastOrDefault()); ; if (res != null) Response.MakeGetResponse(res, "application/json"); else Response.MakeErrorResponse(); + break; } #endregion - else + #region PlayerLookup Service + //Doesn't pass in SessionId!! + else if (absolutepath.Contains($"/{LoginGUID}/person/byDisplayName")) { - //First check if this is a Inventory request - if (absolutepath.Contains($"/RewardsService/") && absolutepath.Contains("counts")) + var res = playerLookupService.HandlePlayerLookupService(absolutepath); + if (!string.IsNullOrEmpty(res)) + Response.MakeGetResponse(res, "application/json"); + else + Response.MakeErrorResponse(404, "Not Found"); + break; + } + #endregion + + #region DEBUG AchievementService + else if (absolutepath.Contains($"/AchievementService/{SSFWUserSessionManager.GetIdBySessionId(sessionid)}")) + { + var res = achievementService.HandleAchievementService(absolutepath); + if (!string.IsNullOrEmpty(res)) + Response.MakeGetResponse(res, "application/json"); + else + Response.MakeErrorResponse(404, "Not Found"); + break; + } + #endregion + + #region DEBUG AuditService + if (absolutepath.Contains($"/AuditService/log/{env}/{SSFWUserSessionManager.GetIdBySessionId(sessionid)}/counts") + || absolutepath.Contains($"/AuditService/log/{env}/{SSFWUserSessionManager.GetIdBySessionId(sessionid)}/object")) + { + var res = auditService.HandleAuditService(absolutepath, Array.Empty(), request); + + if (!string.IsNullOrEmpty(res)) + Response.MakeGetResponse(res, "application/json"); + else + Response.MakeErrorResponse(404, "Not Found"); + break; + } + #endregion + + #region RewardService Inventory System + //First check if this is a Inventory request + if (absolutepath.Contains($"/RewardsService/") && absolutepath.Contains("counts")) + { + //Detect if existing inv exists + if (File.Exists(filePath + ".json")) { - //Detect if existing inv exists - if (File.Exists(filePath + ".json")) - { - string? res = FileHelper.ReadAllText(filePath + ".json", _legacykey); + string? res = FileHelper.ReadAllText(filePath + ".json", _legacykey); - if (!string.IsNullOrEmpty(res)) - { - if (GetHeaderValue(Headers, "Accept") == "application/json") - Response.MakeGetResponse(res, "application/json"); - else - Response.MakeGetResponse(res); - } + if (!string.IsNullOrEmpty(res)) + { + if (GetHeaderValue(Headers, "Accept") == "application/json") + Response.MakeGetResponse(res, "application/json"); else - Response.MakeErrorResponse(); + Response.MakeGetResponse(res); } - else //fallback default - Response.MakeGetResponse(@"{ ""00000000-00000000-00000000-00000001"": 1 } ", "application/json"); + else + Response.MakeErrorResponse(); } - //Check for specifically the Tracking GUID - else if (absolutepath.Contains($"/RewardsService/") && absolutepath.Contains("object/00000000-00000000-00000000-00000001")) + else //fallback default + Response.MakeGetResponse(@"{ ""00000000-00000000-00000000-00000001"": 1 } ", "application/json"); + break; + } + //Check for specifically the Tracking GUID + else if (absolutepath.Contains($"/RewardsService/") && absolutepath.Contains("object/00000000-00000000-00000000-00000001")) + { + //Detect if existing inv exists + if (File.Exists(filePath + ".json")) { - //Detect if existing inv exists - if (File.Exists(filePath + ".json")) - { - string? res = FileHelper.ReadAllText(filePath + ".json", _legacykey); + string? res = FileHelper.ReadAllText(filePath + ".json", _legacykey); - if (!string.IsNullOrEmpty(res)) - { - if (GetHeaderValue(Headers, "Accept") == "application/json") - Response.MakeGetResponse(res, "application/json"); - else - Response.MakeGetResponse(res); - } + if (!string.IsNullOrEmpty(res)) + { + if (GetHeaderValue(Headers, "Accept") == "application/json") + Response.MakeGetResponse(res, "application/json"); else - Response.MakeErrorResponse(); + Response.MakeGetResponse(res); } - else //fallback default - { + else + Response.MakeErrorResponse(); + } + else //fallback default + { #if DEBUG - LoggerAccessor.LogWarn($"[SSFWProcessor] : {UserAgent} Non-existent inventories detected, using defaults!"); + LoggerAccessor.LogWarn($"[SSFWProcessor] : {UserAgent} Non-existent inventories detected, using defaults!"); #endif - if (absolutepath.Contains("p4t-cprod")) - { - #region Quest for Greatness - Response.MakeGetResponse(@"{ + if (absolutepath.Contains("p4t-cprod")) + { + #region Quest for Greatness + Response.MakeGetResponse(@"{ ""result"": 0, ""rewards"": { ""00000000-00000000-00000000-00000001"": { @@ -300,12 +347,12 @@ private static HttpResponse SSFWRequestProcess(HttpRequest request, HttpResponse } } }", "application/json"); - #endregion - } - else - { - #region Pottermore - Response.MakeGetResponse(@"{ + #endregion + } + else + { + #region Pottermore + Response.MakeGetResponse(@"{ ""result"": 0, ""rewards"": [ { @@ -316,71 +363,70 @@ private static HttpResponse SSFWRequestProcess(HttpRequest request, HttpResponse } ] }", "application/json"); - #endregion - } - + #endregion } + break; } - else if (absolutepath.Contains($"/ClanService/{env}/clan/")) - clanService.HandleClanDetailsService(request, Response, absolutepath); - else if (File.Exists(filePath + ".json")) - { - string? res = FileHelper.ReadAllText(filePath + ".json", _legacykey); + } + #endregion - if (!string.IsNullOrEmpty(res)) - { - if (GetHeaderValue(Headers, "Accept") == "application/json") - Response.MakeGetResponse(res, "application/json"); - else - Response.MakeGetResponse(res); - } - else - Response.MakeErrorResponse(); - } - else if (File.Exists(filePath + ".bin")) - { - byte[]? res = FileHelper.ReadAllBytes(filePath + ".bin", _legacykey); + #region ClanService + else if (absolutepath.Contains($"/ClanService/{env}/clan/")) + clanService.HandleClanDetailsService(request, Response, absolutepath); + #endregion - if (res != null) - Response.MakeGetResponse(res, "application/octet-stream"); - else - Response.MakeErrorResponse(); - } - else if (File.Exists(filePath + ".jpeg")) - { - byte[]? res = FileHelper.ReadAllBytes(filePath + ".jpeg", _legacykey); + #region File return JSON, bin, jpeg + else if (File.Exists(filePath + ".json")) + { + string? res = FileHelper.ReadAllText(filePath + ".json", _legacykey); - if (res != null) - Response.MakeGetResponse(res, "image/jpeg"); + if (!string.IsNullOrEmpty(res)) + { + if (GetHeaderValue(Headers, "Accept") == "application/json") + Response.MakeGetResponse(res, "application/json"); else - Response.MakeErrorResponse(); + Response.MakeGetResponse(res); } else - { - LoggerAccessor.LogWarn($"[SSFWProcessor] : {UserAgent} Requested a non-existent file - {filePath}"); - Response.Clear(); - Response.SetBegin((int)HttpStatusCode.NotFound); - Response.SetBody(); - } + Response.MakeErrorResponse(); } - } - else if (absolutepath.Contains($"/SaveDataService/avatar/{env}/") && absolutepath.EndsWith(".jpg")) - { - if (File.Exists(filePath)) + else if (File.Exists(filePath + ".bin")) + { + byte[]? res = FileHelper.ReadAllBytes(filePath + ".bin", _legacykey); + + if (res != null) + Response.MakeGetResponse(res, "application/octet-stream"); + else + Response.MakeErrorResponse(); + } + else if (File.Exists(filePath + ".jpeg")) { - byte[]? res = FileHelper.ReadAllBytes(filePath, _legacykey); + byte[]? res = FileHelper.ReadAllBytes(filePath + ".jpeg", _legacykey); if (res != null) - Response.MakeGetResponse(res, "image/jpg"); + Response.MakeGetResponse(res, "image/jpeg"); else Response.MakeErrorResponse(); } else { + LoggerAccessor.LogWarn($"[SSFWProcessor] : {UserAgent} Requested a non-existent file - {filePath}"); Response.Clear(); Response.SetBegin((int)HttpStatusCode.NotFound); Response.SetBody(); } + #endregion + } + + #region SaveData AvatarService + else if (absolutepath.Contains($"/SaveDataService/avatar/{env}/") + && absolutepath.EndsWith(".jpg")) + { + byte[]? res = avatarService.HandleAvatarService(filePath, _legacykey); + if (res != null) + Response.MakeGetResponse(res, "image/jpg"); + else + Response.MakeErrorResponse(404, "Not Found"); } else { @@ -389,11 +435,13 @@ private static HttpResponse SSFWRequestProcess(HttpRequest request, HttpResponse Response.SetBody(); } break; + #endregion + case "POST": if (request.BodyLength <= Array.MaxLength) { - #region SSFW Login + #region IdentityService Login byte[] postbuffer = request.BodyBytes; if (absolutepath == $"/{LoginGUID}/login/token/psn") { @@ -402,7 +450,7 @@ private static HttpResponse SSFWRequestProcess(HttpRequest request, HttpResponse if (!string.IsNullOrEmpty(XHomeClientVersion) && !string.IsNullOrEmpty(generalsecret)) { - SSFWLogin login = new(XHomeClientVersion, generalsecret, XHomeClientVersion.Replace(".", string.Empty).PadRight(6, '0'), GetHeaderValue(Headers, "x-signature"), _legacykey); + IdentityService login = new(XHomeClientVersion, generalsecret, XHomeClientVersion.Replace(".", string.Empty).PadRight(6, '0'), GetHeaderValue(Headers, "x-signature"), _legacykey); string? result = login.HandleLogin(postbuffer, env); if (!string.IsNullOrEmpty(result)) { @@ -423,32 +471,14 @@ private static HttpResponse SSFWRequestProcess(HttpRequest request, HttpResponse } #endregion - #region PING + #region PING KeepAlive Service else if (absolutepath.Contains("/morelife") && !string.IsNullOrEmpty(GetHeaderValue(Headers, "x-signature"))) { - const byte GuidLength = 36; - int index = absolutepath.IndexOf("/morelife"); - - if (index != -1 && index > GuidLength) // Makes sure we have at least 36 chars available beforehand. - { - // Extract the substring between the last '/' and the morelife separator. - string resultSessionId = absolutepath.Substring(index - GuidLength, GuidLength); - - if (Regex.IsMatch(resultSessionId, @"^[{(]?([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})[)}]?$") && IsSSFWRegistered(resultSessionId)) - { - SSFWUserSessionManager.UpdateKeepAliveTime(resultSessionId); - Response.MakeGetResponse("{}", "application/json"); - break; - } - else - { - Response.Clear(); - Response.SetBegin((int)HttpStatusCode.Forbidden); - Response.SetBody(); - } - } + if (KeepAliveService.UpdateKeepAliveForClient(absolutepath)) + Response.MakeOkResponse(); // This doesn't even need a empty array, simply 200 Status is enough. + //Response.MakeGetResponse("{}", "application/json"); else - Response.MakeErrorResponse(); + Response.MakeErrorResponse(403); } #endregion @@ -492,13 +522,38 @@ private static HttpResponse SSFWRequestProcess(HttpRequest request, HttpResponse Response.MakeOkResponse(); } else if ( - absolutepath.Contains($"/RewardsService/pm_{env}_inv/") + absolutepath.Contains($"/RewardsService/pm_{env}_cards/") || absolutepath.Contains($"/RewardsService/pmcards/") || absolutepath.Contains($"/RewardsService/p4t-{env}/")) Response.MakeGetResponse(rewardSvc.HandleRewardServiceInvPOST(postbuffer, directoryPath, filePath, absolutepath), "application/json"); #endregion + + #region ClanService else if (absolutepath.Contains($"/ClanService/{env}/clan/")) clanService.HandleClanDetailsService(request, Response, absolutepath); + #endregion + + #region TradingService + else if (absolutepath.Contains($"/TradingService/pmcards/trade")) + { + string? res = tradingService.HandleTradingService(request, sessionid, absolutepath); + + if (res != null) + Response.MakeGetResponse(res, "application/json"); + else + Response.MakeErrorResponse(); + break; + } + #endregion + + #region FriendsService + else if (absolutepath.Contains($"/identity/person/{sessionid}/data/psn/friends-list")) + { + var res = friendsService.HandleFriendsService(absolutepath, postbuffer); + Response.MakeOkResponse(); + } + #endregion + else { LoggerAccessor.LogWarn($"[SSFWProcessor] : Host requested a POST method I don't know about! - Report it to GITHUB with the request : {absolutepath}"); @@ -564,12 +619,16 @@ private static HttpResponse SSFWRequestProcess(HttpRequest request, HttpResponse Response.MakeErrorResponse(); break; case "application/json": + + #region Event Log AuditService if (absolutepath.Equals("/AuditService/log")) { - auditService.HandleAuditService(absolutepath, putbuffer); - //Audit doesn't care we send ok! + auditService.HandleAuditService(absolutepath, putbuffer, request); + //Audit doesn't care so we send ok! Response.MakeOkResponse(); } + #endregion + else { File.WriteAllBytes($"{SSFWServerConfiguration.SSFWStaticFolder}/{absolutepath}.json", putbuffer); @@ -627,8 +686,35 @@ private static HttpResponse SSFWRequestProcess(HttpRequest request, HttpResponse } } #endregion + + #region ClanService else if (absolutepath.Contains($"/ClanService/{env}/clan/")) + { clanService.HandleClanDetailsService(request, Response, absolutepath); + } + #endregion + + #region RewardsService Inventory DEBUG + // RewardsService Inventory DEBUG - WipeInventory + else if (absolutepath.Contains($"/RewardsService/pmcards/rewards/{SSFWUserSessionManager.GetIdBySessionId(sessionid)}")) + { + var res = rewardSvc.HandleRewardServiceWipeInvDELETE(directoryPath, filePath, absolutepath, UserAgent, sessionid); + if (res != null) + Response.MakeOkResponse(); + else + Response.MakeErrorResponse(500, "Failed to Delete Rewards Inventory!"); + } + // RewardsService Inventory DEBUG - DebugClearCardTrackingData + else if (absolutepath.Contains($"/RewardsService/pmcards/rewards/{SSFWUserSessionManager.GetIdBySessionId(sessionid)}/00000000-00000000-00000000-00000001")) + { + var res = rewardSvc.HandleRewardServiceInvCardTrackingDataDELETE(directoryPath, filePath, absolutepath, UserAgent, sessionid); + if (res != null) + Response.MakeOkResponse(); + else + Response.MakeErrorResponse(500, "Failed to Delete Rewards Card Tracking Data!"); + } + #endregion + else { if (File.Exists(filePath + ".json")) @@ -669,7 +755,8 @@ private static HttpResponse SSFWRequestProcess(HttpRequest request, HttpResponse break; } } - //SoundShapes + + #region SoundShapes else if (UserAgent.Contains("PS3Application")) { isApiRequest = true; @@ -684,34 +771,43 @@ private static HttpResponse SSFWRequestProcess(HttpRequest request, HttpResponse { if (request.BodyLength <= Array.MaxLength) { - #region SSFW Login + #region IdentityService Login byte[] postbuffer = request.BodyBytes; //SoundShapes Login if (absolutepath == "/identity/login/token/psn") { - SSFWLogin login = new("1.00", "SoundShapes", "1.14", "$ound$h@pesi$C00l", _legacykey); + IdentityService login = new("1.00", "SoundShapes", "1.14", "$ound$h@pesi$C00l", _legacykey); string? result = login.HandleLoginSS(postbuffer, "cprod"); if (!string.IsNullOrEmpty(result)) { Response.Clear(); Response.SetBegin(201); // Replace with URL or proper Server IP Response.SetHeader("location", $"http://{IPAddress.Any}/_dentity/api/service/{LoginGUID}/proxy/login/token/psn/api/client/sessions/f59306bd-3e25-4a34-a41c-ae6c0744c57e"); - Response.SetHeader("X-OTG-Identity-SessionId", "f59306bd-3e25-4a34-a41c-ae6c0744c57e"); + Response.SetHeader("X-OTG-Identity-SessionId", sessionid); Response.SetContentType("application/json"); Response.SetBody(result, encoding); } else Response.MakeErrorResponse(); } - else if (absolutepath.Contains("/identity/person")) + #endregion + + #region FriendService + else if (absolutepath.Contains($"/identity/person/{sessionid}/data/psn/friends-list")) + { + FriendsService friendsService = new(sessionid, "prod", _legacykey); + var res = friendsService.HandleFriendsService(absolutepath, postbuffer); Response.MakeOkResponse(); + } #endregion } break; } } } + #endregion + } if (!isApiRequest) @@ -797,11 +893,12 @@ private static HttpResponse SSFWRequestProcess(HttpRequest request, HttpResponse break; case "/WebService/ApplyLayoutOverride/": sessionId = GetHeaderValue(Headers, "sessionid", false); + if (sessionId == string.Empty) sessionId = GetHeaderValue(Headers, "X-Home-Session-Id", false); string targetUserName = GetHeaderValue(Headers, "targetUserName", false); string sceneId = GetHeaderValue(Headers, "sceneId", false); env = GetHeaderValue(Headers, "env", false); - if (string.IsNullOrEmpty(env) || !SSFWMisc.homeEnvs.Contains(env)) + if (string.IsNullOrEmpty(env) || !SSFWServerConfiguration.homeEnvs.Contains(env)) env = "cprod"; Response.Clear(); @@ -850,7 +947,7 @@ private static HttpResponse SSFWRequestProcess(HttpRequest request, HttpResponse } if (!string.IsNullOrEmpty(matchingDirectory)) - res = new SSFWLayoutService(_legacykey).HandleLayoutServiceGET(matchingDirectory, sceneId); + res = new LayoutService(_legacykey).HandleLayoutServiceGET(matchingDirectory, sceneId); } // if the dir not exists, we return 403. @@ -894,6 +991,7 @@ private static HttpResponse SSFWRequestProcess(HttpRequest request, HttpResponse break; case "/WebService/R3moveLayoutOverride/": sessionId = GetHeaderValue(Headers, "sessionid", false); + if (sessionId == string.Empty) sessionId = GetHeaderValue(Headers, "X-Home-Session-Id", false); Response.Clear(); @@ -918,9 +1016,10 @@ private static HttpResponse SSFWRequestProcess(HttpRequest request, HttpResponse break; case "/WebService/GetMini/": sessionId = GetHeaderValue(Headers, "sessionid", false); + if (sessionId == string.Empty) sessionId = GetHeaderValue(Headers, "X-Home-Session-Id", false); env = GetHeaderValue(Headers, "env", false); - if (string.IsNullOrEmpty(env) || !SSFWMisc.homeEnvs.Contains(env)) + if (string.IsNullOrEmpty(env) || !SSFWServerConfiguration.homeEnvs.Contains(env)) env = "cprod"; userId = SSFWUserSessionManager.GetIdBySessionId(sessionId); @@ -962,9 +1061,10 @@ private static HttpResponse SSFWRequestProcess(HttpRequest request, HttpResponse case "/WebService/AddMiniItem/": uuid = GetHeaderValue(Headers, "uuid", false); sessionId = GetHeaderValue(Headers, "sessionid", false); + if (sessionId == string.Empty) sessionId = GetHeaderValue(Headers, "X-Home-Session-Id", false); env = GetHeaderValue(Headers, "env", false); - if (string.IsNullOrEmpty(env) || !SSFWMisc.homeEnvs.Contains(env)) + if (string.IsNullOrEmpty(env) || !SSFWServerConfiguration.homeEnvs.Contains(env)) env = "cprod"; userId = SSFWUserSessionManager.GetIdBySessionId(sessionId); @@ -977,7 +1077,7 @@ private static HttpResponse SSFWRequestProcess(HttpRequest request, HttpResponse { try { - new SSFWRewardsService(_legacykey).AddMiniEntry(uuid, InventoryEntryType, $"{SSFWServerConfiguration.SSFWStaticFolder}/RewardsService/trunks-{env}/trunks/{userId}.json", env, userId); + new RewardsService(_legacykey).AddMiniEntry(uuid, InventoryEntryType, $"{SSFWServerConfiguration.SSFWStaticFolder}/RewardsService/trunks-{env}/trunks/{userId}.json", env, userId); Response.Clear(); Response.SetBegin((int)HttpStatusCode.OK); Response.SetBody($"UUID: {uuid} successfully added to the Mini rewards list.", encoding, GetHeaderValue(Headers, "Origin")); @@ -1008,9 +1108,10 @@ private static HttpResponse SSFWRequestProcess(HttpRequest request, HttpResponse case "/WebService/AddMiniItems/": uuids = GetHeaderValue(Headers, "uuids", false).Split(','); sessionId = GetHeaderValue(Headers, "sessionid", false); + if (sessionId == string.Empty) sessionId = GetHeaderValue(Headers, "X-Home-Session-Id", false); env = GetHeaderValue(Headers, "env", false); - if (string.IsNullOrEmpty(env) || !SSFWMisc.homeEnvs.Contains(env)) + if (string.IsNullOrEmpty(env) || !SSFWServerConfiguration.homeEnvs.Contains(env)) env = "cprod"; userId = SSFWUserSessionManager.GetIdBySessionId(sessionId); @@ -1030,7 +1131,7 @@ private static HttpResponse SSFWRequestProcess(HttpRequest request, HttpResponse try { - new SSFWRewardsService(_legacykey).AddMiniEntries(entriesToAdd, $"{SSFWServerConfiguration.SSFWStaticFolder}/RewardsService/trunks-{env}/trunks/{userId}.json", env, userId); + new RewardsService(_legacykey).AddMiniEntries(entriesToAdd, $"{SSFWServerConfiguration.SSFWStaticFolder}/RewardsService/trunks-{env}/trunks/{userId}.json", env, userId); Response.Clear(); Response.SetBegin((int)HttpStatusCode.OK); Response.SetBody($"UUIDs: {string.Join(",", uuids)} successfully added to the Mini rewards list.", encoding, GetHeaderValue(Headers, "Origin")); @@ -1061,9 +1162,10 @@ private static HttpResponse SSFWRequestProcess(HttpRequest request, HttpResponse case "/WebService/RemoveMiniItem/": uuid = GetHeaderValue(Headers, "uuid", false); sessionId = GetHeaderValue(Headers, "sessionid", false); + if (sessionId == string.Empty) sessionId = GetHeaderValue(Headers, "X-Home-Session-Id", false); env = GetHeaderValue(Headers, "env", false); - if (string.IsNullOrEmpty(env) || !SSFWMisc.homeEnvs.Contains(env)) + if (string.IsNullOrEmpty(env) || !SSFWServerConfiguration.homeEnvs.Contains(env)) env = "cprod"; userId = SSFWUserSessionManager.GetIdBySessionId(sessionId); @@ -1076,7 +1178,7 @@ private static HttpResponse SSFWRequestProcess(HttpRequest request, HttpResponse { try { - new SSFWRewardsService(_legacykey).RemoveMiniEntry(uuid, InventoryEntryType, $"{SSFWServerConfiguration.SSFWStaticFolder}/RewardsService/trunks-{env}/trunks/{userId}.json", env, userId); + new RewardsService(_legacykey).RemoveMiniEntry(uuid, InventoryEntryType, $"{SSFWServerConfiguration.SSFWStaticFolder}/RewardsService/trunks-{env}/trunks/{userId}.json", env, userId); Response.Clear(); Response.SetBegin((int)HttpStatusCode.OK); Response.SetBody($"UUID: {uuid} successfully removed in the Mini rewards list.", encoding, GetHeaderValue(Headers, "Origin")); @@ -1107,9 +1209,10 @@ private static HttpResponse SSFWRequestProcess(HttpRequest request, HttpResponse case "/WebService/RemoveMiniItems/": uuids = GetHeaderValue(Headers, "uuids", false).Split(','); sessionId = GetHeaderValue(Headers, "sessionid", false); + if (sessionId == string.Empty) sessionId = GetHeaderValue(Headers, "X-Home-Session-Id", false); env = GetHeaderValue(Headers, "env", false); - if (string.IsNullOrEmpty(env) || !SSFWMisc.homeEnvs.Contains(env)) + if (string.IsNullOrEmpty(env) || !SSFWServerConfiguration.homeEnvs.Contains(env)) env = "cprod"; userId = SSFWUserSessionManager.GetIdBySessionId(sessionId); @@ -1129,7 +1232,7 @@ private static HttpResponse SSFWRequestProcess(HttpRequest request, HttpResponse try { - new SSFWRewardsService(_legacykey).RemoveMiniEntries(entriesToRemove, $"{SSFWServerConfiguration.SSFWStaticFolder}/RewardsService/trunks-{env}/trunks/{userId}.json", env, userId); + new RewardsService(_legacykey).RemoveMiniEntries(entriesToRemove, $"{SSFWServerConfiguration.SSFWStaticFolder}/RewardsService/trunks-{env}/trunks/{userId}.json", env, userId); Response.Clear(); Response.SetBegin((int)HttpStatusCode.OK); Response.SetBody($"UUIDs: {string.Join(",", uuids)} removed in the Mini rewards list.", encoding, GetHeaderValue(Headers, "Origin")); diff --git a/Servers/SSFWServer/SSFWServerConfiguration.cs b/Servers/SSFWServer/SSFWServerConfiguration.cs new file mode 100644 index 000000000..60cea7bcd --- /dev/null +++ b/Servers/SSFWServer/SSFWServerConfiguration.cs @@ -0,0 +1,147 @@ +using CustomLogger; +using Newtonsoft.Json.Linq; +using System.Security.Cryptography; +using MultiServerLibrary.Extension; +using System.Collections.Concurrent; + +public static class SSFWServerConfiguration +{ + public static bool SSFWCrossSave { get; set; } = true; + public static bool EnableHTTPCompression { get; set; } = false; + public static int SSFWTTL { get; set; } = 180; + public static string SSFWMinibase { get; set; } = "[]"; + public static string SSFWLegacyKey { get; set; } = "**NoNoNoYouCantHaxThis****69"; + public static string SSFWSessionIdKey { get; set; } = StringUtils.GenerateRandomBase64KeyAsync().Result; + public static string SSFWLayoutsFolder { get; set; } = $"{Directory.GetCurrentDirectory()}/static/layouts"; + public static string SSFWStaticFolder { get; set; } = $"{Directory.GetCurrentDirectory()}/static/wwwssfwroot"; + public static string HTTPSCertificateFile { get; set; } = $"{Directory.GetCurrentDirectory()}/static/SSL/SSFW.pfx"; + public static string HTTPSCertificatePassword { get; set; } = "qwerty"; + public static HashAlgorithmName HTTPSCertificateHashingAlgorithm { get; set; } = HashAlgorithmName.SHA384; + public static string ScenelistFile { get; set; } = $"{Directory.GetCurrentDirectory()}/static/wwwssfwroot/SceneList.xml"; + public static string[]? HTTPSDNSList { get; set; } = { + "cprod.homerewards.online.scee.com", + "cprod.homeidentity.online.scee.com", + "cprod.homeserverservices.online.scee.com", + "cdev.homerewards.online.scee.com", + "cdev.homeidentity.online.scee.com", + "cdev.homeserverservices.online.scee.com", + "cdevb.homerewards.online.scee.com", + "cdevb.homeidentity.online.scee.com", + "cdevb.homeserverservices.online.scee.com", + "nonprod1.homerewards.online.scee.com", + "nonprod1.homeidentity.online.scee.com", + "nonprod1.homeserverservices.online.scee.com", + "nonprod2.homerewards.online.scee.com", + "nonprod2.homeidentity.online.scee.com", + "nonprod2.homeserverservices.online.scee.com", + "nonprod3.homerewards.online.scee.com", + "nonprod3.homeidentity.online.scee.com", + "nonprod3.homeserverservices.online.scee.com", + "nonprod4.homerewards.online.scee.com", + "nonprod4.homeidentity.online.scee.com", + "nonprod4.homeserverservices.online.scee.com", + }; + + // Sandbox Environments + public static readonly ConcurrentBag homeEnvs = new() + { + "cprod", "cprodts", "cpreprod", "cpreprodb", + "rc-qa", "rcdev", "rc-dev", "cqa-e", + "cqa-a", "cqa-j", "cqa-h", "cqab-e", + "cqab-a", "cqab-j", "cqab-h", "qcqa-e", + "qcqa-a", "qcqa-j", "qcqa-h", "qcpreprod", + "qcqab-e", "qcqab-a", "qcqab-j", "qcqab-h", + "qcpreprodb", "coredev", "core-dev", "core-qa", + "cdev", "cdev2", "cdev3", "cdeva", "cdevb", "cdevc", + "nonprod1", "nonprod2", "nonprod3", "prodsp" + }; + + /// + /// Tries to load the specified configuration file. + /// Throws an exception if it fails to find the file. + /// + /// + /// + public static void RefreshVariables(string configPath) + { + // Make sure the file exists + if (!File.Exists(configPath)) + { + LoggerAccessor.LogWarn($"Could not find the configuration file:{configPath}, writing and using server's default."); + + Directory.CreateDirectory(Path.GetDirectoryName(configPath) ?? Directory.GetCurrentDirectory() + "/static"); + + // Write the JObject to a file + File.WriteAllText(configPath, new JObject( + new JProperty("config_version", (ushort)3), + new JProperty("minibase", SSFWMinibase), + new JProperty("legacyKey", SSFWLegacyKey), + new JProperty("sessionidKey", SSFWSessionIdKey), + new JProperty("time_to_live", SSFWTTL), + new JProperty("cross_save", SSFWCrossSave), + new JProperty("enable_http_compression", EnableHTTPCompression), + new JProperty("static_folder", SSFWStaticFolder), + new JProperty("https_dns_list", HTTPSDNSList ?? Array.Empty()), + new JProperty("certificate_file", HTTPSCertificateFile), + new JProperty("certificate_password", HTTPSCertificatePassword), + new JProperty("certificate_hashing_algorithm", HTTPSCertificateHashingAlgorithm.Name), + new JProperty("scenelist_file", ScenelistFile), + new JProperty("layouts_folder", SSFWLayoutsFolder) + ).ToString()); + + return; + } + + try + { + // Parse the JSON configuration + dynamic config = JObject.Parse(File.ReadAllText(configPath)); + + ushort config_version = GetValueOrDefault(config, "config_version", (ushort)0); + if (config_version >= 2) + EnableHTTPCompression = GetValueOrDefault(config, "enable_http_compression", EnableHTTPCompression); + SSFWMinibase = GetValueOrDefault(config, "minibase", SSFWMinibase); + SSFWTTL = GetValueOrDefault(config, "time_to_live", SSFWTTL); + SSFWLegacyKey = GetValueOrDefault(config, "legacyKey", SSFWLegacyKey); + SSFWSessionIdKey = GetValueOrDefault(config, "sessionidKey", SSFWSessionIdKey); + SSFWCrossSave = GetValueOrDefault(config, "cross_save", SSFWCrossSave); + SSFWStaticFolder = GetValueOrDefault(config, "static_folder", SSFWStaticFolder); + HTTPSCertificateFile = GetValueOrDefault(config, "certificate_file", HTTPSCertificateFile); + HTTPSCertificatePassword = GetValueOrDefault(config, "certificate_password", HTTPSCertificatePassword); + HTTPSCertificateHashingAlgorithm = new HashAlgorithmName(GetValueOrDefault(config, "certificate_hashing_algorithm", HTTPSCertificateHashingAlgorithm.Name)); + HTTPSDNSList = GetValueOrDefault(config, "https_dns_list", HTTPSDNSList); + ScenelistFile = GetValueOrDefault(config, "scenelist_file", ScenelistFile); + SSFWLayoutsFolder = GetValueOrDefault(config, "layouts_folder", SSFWLayoutsFolder); + } + catch (Exception ex) + { + LoggerAccessor.LogWarn($"{configPath} file is malformed (exception: {ex}), using server's default."); + } + } + + // Helper method to get a value or default value if not present + private static T GetValueOrDefault(dynamic obj, string propertyName, T defaultValue) + { + try + { + if (obj != null) + { + if (obj is JObject jObject) + { + if (jObject.TryGetValue(propertyName, out JToken? value)) + { + T? returnvalue = value.ToObject(); + if (returnvalue != null) + return returnvalue; + } + } + } + } + catch (Exception ex) + { + LoggerAccessor.LogError($"[Program] - GetValueOrDefault thrown an exception: {ex}"); + } + + return defaultValue; + } +} diff --git a/Servers/SSFWServer/SSFWUserSessionManager.cs b/Servers/SSFWServer/SSFWUserSessionManager.cs new file mode 100644 index 000000000..e1c52b440 --- /dev/null +++ b/Servers/SSFWServer/SSFWUserSessionManager.cs @@ -0,0 +1,208 @@ +using CustomLogger; +using SSFWServer.Helpers.RegexHelper; +using System.Collections.Concurrent; + +namespace SSFWServer +{ + public class SSFWUserSessionManager + { + private static ConcurrentDictionary userSessions = new(); + + public static void RegisterUser(string userName, string sessionid, string id, int realuserNameSize) + { + if (userSessions.TryGetValue(sessionid, out (int, UserSession, DateTime) sessionEntry)) + UpdateKeepAliveTime(sessionid, sessionEntry); + else if (userSessions.TryAdd(sessionid, (realuserNameSize, new UserSession { Username = userName, Id = id }, DateTime.Now.AddMinutes(SSFWServerConfiguration.SSFWTTL)))) + LoggerAccessor.LogInfo($"[UserSessionManager] - User '{userName}' successfully registered with SessionId '{sessionid}'."); + else + LoggerAccessor.LogError($"[UserSessionManager] - Failed to register User '{userName}' with SessionId '{sessionid}'."); + } + + public static string? GetSessionIdByUsername(string? userName, bool rpcn) + { + if (string.IsNullOrEmpty(userName)) + return null; + + foreach (var kvp in userSessions) + { + string sessionId = kvp.Key; + var (realSize, session, _) = kvp.Value; + + string? realUsername = session.Username?.Substring(0, realSize); + + if (string.Equals(realUsername + (rpcn ? "@RPCN" : string.Empty), userName, StringComparison.Ordinal)) + return sessionId; + } + + return null; + } + + public static string? GetUsernameBySessionId(string? sessionId) + { + if (string.IsNullOrEmpty(sessionId)) + return null; + + if (userSessions.TryGetValue(sessionId, out (int, UserSession, DateTime) sessionEntry)) + return sessionEntry.Item2.Username; + + return null; + } + + public static string? GetFormatedUsernameBySessionId(string? sessionId) + { + if (string.IsNullOrEmpty(sessionId)) + return null; + + if (userSessions.TryGetValue(sessionId, out (int, UserSession, DateTime) sessionEntry)) + { + string? userName = sessionEntry.Item2.Username; + + if (!string.IsNullOrEmpty(userName) && userName.Length > sessionEntry.Item1) + userName = userName.Substring(0, sessionEntry.Item1); + + return userName; + } + + return null; + } + + /// + /// Retrieves the Id of a user by their Username, if they have an active session. + /// + /// The username to search for (case-sensitive). + /// The user's Id if found and session is active, otherwise null. + public static string? GetIdByUsername(string? userName) + { + if (string.IsNullOrEmpty(userName)) + return null; + + foreach (var entry in userSessions.Values) + { + if (!string.IsNullOrEmpty(entry.Item2.Username)) + + // Check if session is still valid and username matches + if (entry.Item3 > DateTime.Now + && entry.Item2.Username.StartsWith(userName) + && HasMinimumClientVersion(userName)) + { + return entry.Item2.Id; + } + } + + return null; + } + + /// + /// Retrieves the full UserSession object by Username, if they have an active session. + /// + /// The username to search for (case-sensitive). + /// The UserSession if found and active, otherwise null. + public static UserSession? GetUserSessionByUsername(string? userName) + { + if (string.IsNullOrEmpty(userName)) + return null; + + foreach (var entry in userSessions.Values) + { + if (!string.IsNullOrEmpty(entry.Item2.Username)) + + if (entry.Item3 > DateTime.Now + && entry.Item2.Username.StartsWith(userName) + && HasMinimumClientVersion(userName)) + { + return entry.Item2; + } + } + + return null; + } + + + public static string? GetIdBySessionId(string? sessionId) + { + if (string.IsNullOrEmpty(sessionId)) + return null; + + (bool, string?) sessionTuple = IsSessionValid(sessionId, false); + + if (sessionTuple.Item1) + return sessionTuple.Item2; + + return null; + } + + public static bool UpdateKeepAliveTime(string sessionid, (int, UserSession, DateTime) sessionEntry = default) + { + if (sessionEntry == default) + { + if (!userSessions.TryGetValue(sessionid, out sessionEntry)) + return false; + } + + DateTime KeepAliveTime = DateTime.Now.AddMinutes(SSFWServerConfiguration.SSFWTTL); + + sessionEntry.Item3 = KeepAliveTime; + + if (userSessions.ContainsKey(sessionid)) + { + LoggerAccessor.LogInfo($"[SSFWUserSessionManager] - Updating: {sessionEntry.Item2?.Username} session with id: {sessionEntry.Item2?.Id} keep-alive time to:{KeepAliveTime}."); + userSessions[sessionid] = sessionEntry; + return true; + } + + LoggerAccessor.LogError($"[SSFWUserSessionManager] - Failed to update: {sessionEntry.Item2?.Username} session with id: {sessionEntry.Item2?.Id} keep-alive time."); + return false; + } + + + public static (bool, string?) IsSessionValid(string? sessionId, bool cleanupDeadSessions) + { + if (string.IsNullOrEmpty(sessionId)) + return (false, null); + + if (userSessions.TryGetValue(sessionId, out (int, UserSession, DateTime) sessionEntry)) + { + if (sessionEntry.Item3 > DateTime.Now) + return (true, sessionEntry.Item2.Id); + else if (cleanupDeadSessions) + { + // Clean up expired entry. + if (userSessions.TryRemove(sessionId, out sessionEntry)) + LoggerAccessor.LogWarn($"[SSFWUserSessionManager] - Cleaned: {sessionEntry.Item2.Username} session with id: {sessionEntry.Item2.Id}..."); + else + LoggerAccessor.LogError($"[SSFWUserSessionManager] - Failed to clean: {sessionEntry.Item2.Username} session with id: {sessionEntry.Item2.Id}..."); + } + } + + return (false, null); + } + + public static void SessionCleanupLoop(object? state) + { + lock (userSessions) + { + foreach (var sessionId in userSessions.Keys) + { + IsSessionValid(sessionId, true); + } + } + } + + private static bool HasMinimumClientVersion(string username, int minimumVersion = 016531) + { + var match = GUIDValidator.VersionFilter.Match(username); + if (match.Success && int.TryParse(match.Value, out int version)) + { + return version >= minimumVersion; + } + return false; + } + } + + public class UserSession + { + public string? Username { get; set; } + public string? Id { get; set; } + } + +} \ No newline at end of file diff --git a/Servers/SSFWServer/Services/AchievementService.cs b/Servers/SSFWServer/Services/AchievementService.cs new file mode 100644 index 000000000..89a18e0e0 --- /dev/null +++ b/Servers/SSFWServer/Services/AchievementService.cs @@ -0,0 +1,28 @@ +using CustomLogger; + +namespace SSFWServer.Services +{ + public class AchievementService + { + private string? sessionid; + private string? env; + private string? key; + + public AchievementService(string sessionid, string env, string? key) + { + this.sessionid = sessionid; + this.env = env; + this.key = key; + } + + public string HandleAchievementService(string absolutePath) + { + string? userName = SSFWUserSessionManager.GetUsernameBySessionId(sessionid); +#if DEBUG + LoggerAccessor.LogInfo($"[SSFW] AchievementService - Requesting {userName}'s achievements"); +#endif + //We send empty response as status 200 for now + return $"{{}}"; + } + } +} diff --git a/Servers/SSFWServer/Services/SSFWAdminObjectService.cs b/Servers/SSFWServer/Services/AdminObjectService.cs similarity index 51% rename from Servers/SSFWServer/Services/SSFWAdminObjectService.cs rename to Servers/SSFWServer/Services/AdminObjectService.cs index 035e251ea..b269fa954 100644 --- a/Servers/SSFWServer/Services/SSFWAdminObjectService.cs +++ b/Servers/SSFWServer/Services/AdminObjectService.cs @@ -1,15 +1,16 @@ using CustomLogger; using Newtonsoft.Json; +using SSFWServer.Helpers; using SSFWServer.Helpers.FileHelper; namespace SSFWServer.Services { - public class SSFWAdminObjectService + public class AdminObjectService { private string? sessionid; private string? key; - public SSFWAdminObjectService(string sessionid, string? key) + public AdminObjectService(string sessionid, string? key) { this.sessionid = sessionid; this.key = key; @@ -17,11 +18,18 @@ public SSFWAdminObjectService(string sessionid, string? key) public bool HandleAdminObjectService(string UserAgent) { - string? username = SSFWUserSessionManager.GetUsernameBySessionId(sessionid); + return IsAdminVerified(UserAgent); + } + + //Helper function for other uses in SSFW services + public bool IsAdminVerified(string userAgent) + { + string? userName = SSFWUserSessionManager.GetUsernameBySessionId(sessionid); + string accountFilePath = $"{SSFWServerConfiguration.SSFWStaticFolder}/SSFW_Accounts/{userName}.json"; - if (!string.IsNullOrEmpty(username) && File.Exists($"{SSFWServerConfiguration.SSFWStaticFolder}/SSFW_Accounts/{username}.json")) + if (!string.IsNullOrEmpty(userName) && File.Exists(accountFilePath)) { - string? userprofiledata = FileHelper.ReadAllText($"{SSFWServerConfiguration.SSFWStaticFolder}/SSFW_Accounts/{username}.json", key); + string? userprofiledata = FileHelper.ReadAllText(accountFilePath, key); if (!string.IsNullOrEmpty(userprofiledata)) { @@ -30,11 +38,11 @@ public bool HandleAdminObjectService(string UserAgent) if (userData != null) { - LoggerAccessor.LogInfo($"[SSFW] - IGA Request from : {UserAgent}/{username} - IGA status : {userData.IGA}"); + LoggerAccessor.LogInfo($"[SSFW] - IsAdminVerified : IGA Request from : {userAgent}/{userName} - IGA status : {userData.IGA}"); if (userData.IGA == 1) { - LoggerAccessor.LogInfo($"[SSFW] - Admin role confirmed for : {UserAgent}/{username}"); + LoggerAccessor.LogInfo($"[SSFW] - IsAdminVerified : Admin role confirmed for : {userAgent}/{userName}"); return true; } @@ -42,7 +50,7 @@ public bool HandleAdminObjectService(string UserAgent) } } - LoggerAccessor.LogError($"[SSFW] - {UserAgent} requested IGA access, but no access allowed so we forbid!"); + LoggerAccessor.LogError($"[SSFW] - IsAdminVerified : IGA Access denied for {userAgent}!"); return false; } diff --git a/Servers/SSFWServer/Services/AuditService.cs b/Servers/SSFWServer/Services/AuditService.cs new file mode 100644 index 000000000..601a912c6 --- /dev/null +++ b/Servers/SSFWServer/Services/AuditService.cs @@ -0,0 +1,101 @@ +using CastleLibrary.Sony.SSFW; +using CustomLogger; +using NetCoreServer; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System.Text; + +namespace SSFWServer.Services +{ + public class AuditService + { + private string? sessionid; + private string? env; + private string? key; + + public AuditService(string sessionid, string env, string? key) + { + this.sessionid = sessionid; + this.env = env; + this.key = key; + } + + public string HandleAuditService(string absolutepath, byte[] buffer, HttpRequest request) + { + string fileNameGUID = GuidGenerator.SSFWGenerateGuid(sessionid, env); + string? personIdToCompare = SSFWUserSessionManager.GetIdBySessionId(sessionid); + string auditLogPath = $"{SSFWServerConfiguration.SSFWStaticFolder}/{absolutepath}"; + + switch (request.Method) + { + case "PUT": + try + { + Directory.CreateDirectory(auditLogPath); + + File.WriteAllText($"{auditLogPath}/{fileNameGUID}.json", Encoding.UTF8.GetString(buffer)); +#if DEBUG + LoggerAccessor.LogInfo($"[SSFW] AuditService - HandleAuditService Audit event log posted: {fileNameGUID}"); +#endif + return $"{{ \"result\": 0 }}"; + } + catch (Exception ex) + { + LoggerAccessor.LogError($"[SSFW] AuditService - HandleAuditService ERROR caught: \n{ex}"); + return $"{{ \"result\": -1 }}"; + } + case "GET": + + if(absolutepath.Contains("counts")) + { + var files = Directory.GetFiles(auditLogPath.Replace("/counts", "")); + + string newFileMatchingEntry = string.Empty; + + List listOfEventsByUser = new(); + int userEventTotal = 1; + int idxTotal = 0; + foreach (string fileToRead in files) + { + string fileContents = File.ReadAllText(fileToRead); + JObject? jsonContents = JsonConvert.DeserializeObject(fileContents); + if(fileContents != null ) + { + JObject mainFile = JObject.Parse(fileContents); + + var userNameInEvent = mainFile["owner"]; + + if (personIdToCompare == (string?)userNameInEvent) + { + string fileName = Path.GetFileNameWithoutExtension(fileToRead); + if(files.Length == userEventTotal) + { + newFileMatchingEntry = $"\"{fileName}\""; + } else + newFileMatchingEntry = $"\"{fileName}\","; + } + listOfEventsByUser.Add(newFileMatchingEntry); + idxTotal++; + } + } +#if DEBUG + LoggerAccessor.LogInfo($"[SSFW] AuditService - HandleAuditService returning count list of logs for player {personIdToCompare}"); +#endif + return $"{{ \"count\": {idxTotal}, \"events\": {{ {string.Join("", listOfEventsByUser)} }} }}"; + } else if(absolutepath.Contains("object")) + { +#if DEBUG + LoggerAccessor.LogInfo("[SSFW] AuditService - HandleAuditService Event log get " + auditLogPath.Replace("/object", "") + ".json"); +#endif + return File.ReadAllText(auditLogPath.Replace("/object", "") + ".json"); + } + break; + default: + LoggerAccessor.LogError($"[SSFW] AuditService - HandleAuditService Method {request.Method} unhandled!"); + return $"{{ \"result\": -1 }}"; + } + + return string.Empty; + } + } +} \ No newline at end of file diff --git a/Servers/SSFWServer/Services/SSFWAvatarLayoutService.cs b/Servers/SSFWServer/Services/AvatarLayoutService.cs similarity index 83% rename from Servers/SSFWServer/Services/SSFWAvatarLayoutService.cs rename to Servers/SSFWServer/Services/AvatarLayoutService.cs index 99abbdf69..52f5f1505 100644 --- a/Servers/SSFWServer/Services/SSFWAvatarLayoutService.cs +++ b/Servers/SSFWServer/Services/AvatarLayoutService.cs @@ -5,11 +5,11 @@ namespace SSFWServer.Services { - public class SSFWAvatarLayoutService + public class AvatarLayoutService { private string? key; - public SSFWAvatarLayoutService(string sessionid, string? key) + public AvatarLayoutService(string sessionid, string? key) { this.key = key; } @@ -22,13 +22,10 @@ public bool HandleAvatarLayout(byte[] buffer, string directorypath, string filep // Check if the string ends with a number if (regex.IsMatch(absolutepath)) { - // Get the matched number as a string - string numberString = regex.Match(absolutepath).Value; - // Check if the number is valid - if (int.TryParse(numberString, out int number)) + if (int.TryParse(regex.Match(absolutepath).Value, out int number)) { - SSFWUpdateAvatar(directorypath + "/list.json", number, delete); + SSFWUpdateAvatarLayout(directorypath + "/list.json", number, delete); if (delete && File.Exists(filepath + ".json")) { @@ -47,12 +44,12 @@ public bool HandleAvatarLayout(byte[] buffer, string directorypath, string filep return false; } - public void SSFWUpdateAvatar(string filePath, int contentToUpdate, bool delete) + public void SSFWUpdateAvatarLayout(string filePath, int contentToUpdate, bool delete) { + string? json = null; + try { - string? json = null; - if (File.Exists(filePath)) json = FileHelper.ReadAllText(filePath, key); @@ -88,8 +85,8 @@ public void SSFWUpdateAvatar(string filePath, int contentToUpdate, bool delete) } catch (Exception ex) { - LoggerAccessor.LogError($"[SSFW] - SSFWUpdateAvatar errored out with this exception - {ex}"); + LoggerAccessor.LogError($"[SSFW] - SSFWUpdateAvatarLayout errored out with this exception - {ex}"); } } } -} +} \ No newline at end of file diff --git a/Servers/SSFWServer/Services/AvatarService.cs b/Servers/SSFWServer/Services/AvatarService.cs new file mode 100644 index 000000000..da1640139 --- /dev/null +++ b/Servers/SSFWServer/Services/AvatarService.cs @@ -0,0 +1,18 @@ +using SSFWServer.Helpers.FileHelper; + +namespace SSFWServer.Services +{ + public class AvatarService + { + public byte[]? HandleAvatarService(string filePath, string? key) + { + if (File.Exists(filePath)) + { + return FileHelper.ReadAllBytes(filePath, key); + } else + { + return null; + } + } + } +} \ No newline at end of file diff --git a/Servers/SSFWServer/Services/ClanService.cs b/Servers/SSFWServer/Services/ClanService.cs new file mode 100644 index 000000000..2148292b1 --- /dev/null +++ b/Servers/SSFWServer/Services/ClanService.cs @@ -0,0 +1,87 @@ +using NetCoreServer; +using NetHasher; +using MultiServerLibrary.HTTP; +using System.Text; +using System.Text.Json; + +namespace SSFWServer.Services +{ + public class ClanService + { + private readonly string? _sessionid; + + public ClanService(string sessionid) + { + _sessionid = sessionid; + } + + // Handles GET,POST,DELETE on this function + public HttpResponse HandleClanDetailsService(HttpRequest req, HttpResponse res, string absolutepath) + { + string filePath = $"{SSFWServerConfiguration.SSFWStaticFolder}{HTTPProcessor.ParseUriFromAbsolutePath(absolutepath).AbsolutePath}.json"; + + if (req.Method == HttpMethod.Post.ToString()) + { + try + { + if (JsonDocument.Parse(req.Body).RootElement.TryGetProperty("sceneObjectId", out JsonElement idElement)) + { + string? psnClanId = absolutepath.Split("/").LastOrDefault(); + string? directoryPath = Path.GetDirectoryName(filePath); + + if (string.IsNullOrEmpty(psnClanId) || string.IsNullOrEmpty(directoryPath)) + throw new Exception(); + + Directory.CreateDirectory(directoryPath); + + // TODO, extract the proper region. + string jsonToWrite = $@"{{ +""region"": ""en-US"", +""message"": ""OK"", +""result"": 0, +""psnClanId"": {psnClanId}, +""sceneObjectId"": ""{idElement.GetString()!}"", +""personId"": ""{_sessionid}"", +""clanId"": ""{DotNetHasher.ComputeMD5String(Encoding.UTF8.GetBytes(psnClanId))}"" +}}"; + File.WriteAllText(filePath, jsonToWrite); + + return res.MakeGetResponse(jsonToWrite, "application/json"); + } + + } + catch + { + // Not Important. + } + + return res.MakeErrorResponse(400); + } + else if (req.Method == HttpMethod.Get.ToString()) // GET ONLY + { + // If clanId exist, we check json and return that back, otherwise not found so Home POST default + if (File.Exists(filePath)) + return res.MakeGetResponse(File.ReadAllText(filePath), "application/json"); + + return res.MakeErrorResponse(404, "Not Found"); + } + + // Delete clan details if clan requested DELETE method + try + { + if (File.Exists(filePath)) + { + string? sceneObjectId = JsonDocument.Parse(File.ReadAllText(filePath)).RootElement.GetProperty("sceneObjectId").GetString() ?? string.Empty; + File.Delete(filePath); + return res.MakeGetResponse($"{{\"sceneObjectIds\": [\"{sceneObjectId}\"] }}", "application/json"); + } + } + catch + { + // Not Important. + } + + return res.MakeErrorResponse(); + } + } +} \ No newline at end of file diff --git a/Servers/SSFWServer/Services/FriendsService.cs b/Servers/SSFWServer/Services/FriendsService.cs new file mode 100644 index 000000000..df607c6c2 --- /dev/null +++ b/Servers/SSFWServer/Services/FriendsService.cs @@ -0,0 +1,40 @@ +using CustomLogger; +using System.Text; + +namespace SSFWServer.Services +{ + public class FriendsService + { + private string? sessionid; + private string? env; + private string? key; + + public FriendsService(string sessionid, string env, string? key) + { + this.sessionid = sessionid; + this.env = env; + this.key = key; + } + + public string HandleFriendsService(string absolutepath, byte[] buffer) + { + string? userName = SSFWUserSessionManager.GetIdBySessionId(sessionid); + string friendsStorePath = $"{SSFWServerConfiguration.SSFWStaticFolder}/FriendsService/{env}"; + try + { + Directory.CreateDirectory(friendsStorePath); + + File.WriteAllText($"{friendsStorePath}/{userName}.txt", Encoding.UTF8.GetString(buffer)); +#if DEBUG + LoggerAccessor.LogInfo($"[SSFW] FriendsService - HandleFriendsService Friends list posted: {userName} at {$"{friendsStorePath}/{userName}.txt"}"); +#endif + return "Success"; + } + catch (Exception ex) + { + LoggerAccessor.LogError($"[SSFW] FriendsService - HandleFriendsService ERROR caught: \n{ex}"); + return ex.Message; + } + } + } +} \ No newline at end of file diff --git a/Servers/SSFWServer/SSFWLogin.cs b/Servers/SSFWServer/Services/IdentityService.cs similarity index 73% rename from Servers/SSFWServer/SSFWLogin.cs rename to Servers/SSFWServer/Services/IdentityService.cs index bee848b1d..2faca6026 100644 --- a/Servers/SSFWServer/SSFWLogin.cs +++ b/Servers/SSFWServer/Services/IdentityService.cs @@ -1,151 +1,14 @@ +using CastleLibrary.Sony.XI5; using CustomLogger; -using System.Text; -using System.Collections.Concurrent; using NetHasher; +using SSFWServer.Helpers.DataMigrator; +using System.Text; using CastleLibrary.Sony.SSFW; -using CastleLibrary.Sony.XI5; +using SSFWServer.Helpers.FileHelper; -namespace SSFWServer +namespace SSFWServer.Services { - public class SSFWUserSessionManager - { - private static ConcurrentDictionary userSessions = new(); - - public static void RegisterUser(string userName, string sessionid, string id, int realuserNameSize) - { - if (userSessions.TryGetValue(sessionid, out (int, UserSession, DateTime) sessionEntry)) - UpdateKeepAliveTime(sessionid, sessionEntry); - else if (userSessions.TryAdd(sessionid, (realuserNameSize, new UserSession { Username = userName, Id = id }, DateTime.Now.AddMinutes(SSFWServerConfiguration.SSFWTTL)))) - LoggerAccessor.LogInfo($"[UserSessionManager] - User '{userName}' successfully registered with SessionId '{sessionid}'."); - else - LoggerAccessor.LogError($"[UserSessionManager] - Failed to register User '{userName}' with SessionId '{sessionid}'."); - } - - public static string? GetSessionIdByUsername(string? userName, bool rpcn) - { - if (string.IsNullOrEmpty(userName)) - return null; - - foreach (var kvp in userSessions) - { - string sessionId = kvp.Key; - var (realSize, session, _) = kvp.Value; - - string? realUsername = session.Username?.Substring(0, realSize); - - if (string.Equals(realUsername + (rpcn ? "@RPCN" : string.Empty), userName, StringComparison.Ordinal)) - return sessionId; - } - - return null; - } - - public static string? GetUsernameBySessionId(string? sessionId) - { - if (string.IsNullOrEmpty(sessionId)) - return null; - - if (userSessions.TryGetValue(sessionId, out (int, UserSession, DateTime) sessionEntry)) - return sessionEntry.Item2.Username; - - return null; - } - - public static string? GetFormatedUsernameBySessionId(string? sessionId) - { - if (string.IsNullOrEmpty(sessionId)) - return null; - - if (userSessions.TryGetValue(sessionId, out (int, UserSession, DateTime) sessionEntry)) - { - string? userName = sessionEntry.Item2.Username; - - if (!string.IsNullOrEmpty(userName) && userName.Length > sessionEntry.Item1) - userName = userName.Substring(0, sessionEntry.Item1); - - return userName; - } - - return null; - } - - public static string? GetIdBySessionId(string? sessionId) - { - if (string.IsNullOrEmpty(sessionId)) - return null; - - (bool, string?) sessionTuple = IsSessionValid(sessionId, false); - - if (sessionTuple.Item1) - return sessionTuple.Item2; - - return null; - } - - public static bool UpdateKeepAliveTime(string sessionid, (int, UserSession, DateTime) sessionEntry = default) - { - if (sessionEntry == default) - { - if (!userSessions.TryGetValue(sessionid, out sessionEntry)) - return false; - } - - DateTime KeepAliveTime = DateTime.Now.AddMinutes(SSFWServerConfiguration.SSFWTTL); - - sessionEntry.Item3 = KeepAliveTime; - - if (userSessions.ContainsKey(sessionid)) - { - LoggerAccessor.LogInfo($"[SSFWUserSessionManager] - Updating: {sessionEntry.Item2?.Username} session with id: {sessionEntry.Item2?.Id} keep-alive time to:{KeepAliveTime}."); - userSessions[sessionid] = sessionEntry; - return true; - } - - LoggerAccessor.LogError($"[SSFWUserSessionManager] - Failed to update: {sessionEntry.Item2?.Username} session with id: {sessionEntry.Item2?.Id} keep-alive time."); - return false; - } - - public static (bool, string?) IsSessionValid(string? sessionId, bool cleanupDeadSessions) - { - if (string.IsNullOrEmpty(sessionId)) - return (false, null); - - if (userSessions.TryGetValue(sessionId, out (int, UserSession, DateTime) sessionEntry)) - { - if (sessionEntry.Item3 > DateTime.Now) - return (true, sessionEntry.Item2.Id); - else if (cleanupDeadSessions) - { - // Clean up expired entry. - if (userSessions.TryRemove(sessionId, out sessionEntry)) - LoggerAccessor.LogWarn($"[SSFWUserSessionManager] - Cleaned: {sessionEntry.Item2.Username} session with id: {sessionEntry.Item2.Id}..."); - else - LoggerAccessor.LogError($"[SSFWUserSessionManager] - Failed to clean: {sessionEntry.Item2.Username} session with id: {sessionEntry.Item2.Id}..."); - } - } - - return (false, null); - } - - public static void SessionCleanupLoop(object? state) - { - lock (userSessions) - { - foreach (var sessionId in userSessions.Keys) - { - IsSessionValid(sessionId, true); - } - } - } - } - - public class UserSession - { - public string? Username { get; set; } - public string? Id { get; set; } - } - - public class SSFWLogin + public class IdentityService { private string? XHomeClientVersion; private string? generalsecret; @@ -153,7 +16,7 @@ public class SSFWLogin private string? xsignature; private string? key; - public SSFWLogin(string XHomeClientVersion, string generalsecret, string homeClientVersion, string? xsignature, string? key) + public IdentityService(string XHomeClientVersion, string generalsecret, string homeClientVersion, string? xsignature, string? key) { this.XHomeClientVersion = XHomeClientVersion; this.generalsecret = generalsecret; @@ -293,7 +156,7 @@ public SSFWLogin(string XHomeClientVersion, string generalsecret, string homeCli } if (IsRPCN && Directory.Exists($"{SSFWServerConfiguration.SSFWStaticFolder}/AvatarLayoutService/{env}/{ResultStrings.Item2}") && !Directory.Exists($"{SSFWServerConfiguration.SSFWStaticFolder}/AvatarLayoutService/{env}/{ResultStrings.Item1}")) - SSFWDataMigrator.MigrateSSFWData(SSFWServerConfiguration.SSFWStaticFolder, ResultStrings.Item2, ResultStrings.Item1); + DataMigrator.MigrateSSFWData(SSFWServerConfiguration.SSFWStaticFolder, ResultStrings.Item2, ResultStrings.Item1); string? resultString = IsRPCN ? ResultStrings.Item1 : ResultStrings.Item2; @@ -317,7 +180,7 @@ public SSFWLogin(string XHomeClientVersion, string generalsecret, string homeCli if (File.Exists($"{SSFWServerConfiguration.SSFWStaticFolder}/LayoutService/{env}/person/{resultString}/mylayout.json")) // Migrate data. { // Parsing each value in the dictionary - foreach (var kvp in new Services.SSFWLayoutService(key).SSFWGetLegacyFurnitureLayouts($"{SSFWServerConfiguration.SSFWStaticFolder}/LayoutService/{env}/person/{resultString}/mylayout.json")) + foreach (var kvp in new Services.LayoutService(key).SSFWGetLegacyFurnitureLayouts($"{SSFWServerConfiguration.SSFWStaticFolder}/LayoutService/{env}/person/{resultString}/mylayout.json")) { if (kvp.Key == "00000000-00000000-00000000-00000004") { @@ -326,7 +189,7 @@ public SSFWLogin(string XHomeClientVersion, string generalsecret, string homeCli } else { - string scenename = scenemap.FirstOrDefault(x => x.Value == SSFWMisc.ExtractPortion(kvp.Key, 13, 18)).Key; + string scenename = scenemap.FirstOrDefault(x => x.Value == Program.ExtractPortion(kvp.Key, 13, 18)).Key; if (!string.IsNullOrEmpty(scenename)) { if (File.Exists($"{SSFWServerConfiguration.SSFWStaticFolder}/LayoutService/{env}/person/{resultString}/{kvp.Key}.json")) // SceneID now mapped, so SceneID based file has become obsolete. @@ -346,12 +209,14 @@ public SSFWLogin(string XHomeClientVersion, string generalsecret, string homeCli File.Delete($"{SSFWServerConfiguration.SSFWStaticFolder}/LayoutService/{env}/person/{resultString}/mylayout.json"); } else if (!File.Exists($"{SSFWServerConfiguration.SSFWStaticFolder}/LayoutService/{env}/person/{resultString}/HarborStudio.json")) - File.WriteAllText($"{SSFWServerConfiguration.SSFWStaticFolder}/LayoutService/{env}/person/{resultString}/HarborStudio.json", SSFWMisc.HarbourStudioLayout); + File.WriteAllText($"{SSFWServerConfiguration.SSFWStaticFolder}/LayoutService/{env}/person/{resultString}/HarborStudio.json", + File.ReadAllText($"{SSFWServerConfiguration.SSFWLayoutsFolder}/HarborStudio.json")); } else { if (!File.Exists($"{SSFWServerConfiguration.SSFWStaticFolder}/LayoutService/{env}/person/{resultString}/mylayout.json")) - File.WriteAllText($"{SSFWServerConfiguration.SSFWStaticFolder}/LayoutService/{env}/person/{resultString}/mylayout.json", SSFWMisc.LegacyLayoutTemplate); + File.WriteAllText($"{SSFWServerConfiguration.SSFWStaticFolder}/LayoutService/{env}/person/{resultString}/mylayout.json", + File.ReadAllText($"{SSFWServerConfiguration.SSFWLayoutsFolder}/LegacyLayout.json")); } if (!File.Exists($"{SSFWServerConfiguration.SSFWStaticFolder}/RewardsService/{env}/rewards/{resultString}/mini.json")) @@ -505,10 +370,11 @@ public SSFWLogin(string XHomeClientVersion, string generalsecret, string homeCli return null; } - return $"{{\"session\": {{\"expires: 3097114741746 ,\"id\":\"{(IsRPCN ? SessionIDs.Item1 : SessionIDs.Item2)}\",\"person\":{{\"id\":\"{(IsRPCN ? SessionIDs.Item1 : SessionIDs.Item2)}\",\"display_name\":\"{resultString}\"}},\"service\":{{\"id\":\"{(IsRPCN ? SessionIDs.Item1 : SessionIDs.Item2)}\",\"display_name\":\"{resultString}\"}} }} }}"; + return $"{{\"session\": {{\"expires\": \"3097114741746\" ,\"id\":\"{(IsRPCN ? SessionIDs.Item1 : SessionIDs.Item2)}\",\"person\":{{\"id\":\"{(IsRPCN ? SessionIDs.Item1 : SessionIDs.Item2)}\",\"display_name\":\"{resultString}\"}},\"service\":{{\"id\":\"{(IsRPCN ? SessionIDs.Item1 : SessionIDs.Item2)}\",\"display_name\":\"{resultString}\"}} }} }} }}"; } return null; } + } } \ No newline at end of file diff --git a/Servers/SSFWServer/Services/KeepAliveService.cs b/Servers/SSFWServer/Services/KeepAliveService.cs new file mode 100644 index 000000000..86acaed79 --- /dev/null +++ b/Servers/SSFWServer/Services/KeepAliveService.cs @@ -0,0 +1,15 @@ +using SSFWServer.Helpers.RegexHelper; + +namespace SSFWServer.Services +{ + public class KeepAliveService + { + public static bool UpdateKeepAliveForClient(string absolutePath) + { + string resultSessionId = absolutePath.Split("/")[3]; + if (GUIDValidator.RegexSessionValidator.IsMatch(resultSessionId)) + return SSFWUserSessionManager.UpdateKeepAliveTime(resultSessionId); + return false; + } + } +} \ No newline at end of file diff --git a/Servers/SSFWServer/Services/SSFWLayoutService.cs b/Servers/SSFWServer/Services/LayoutService.cs similarity index 91% rename from Servers/SSFWServer/Services/SSFWLayoutService.cs rename to Servers/SSFWServer/Services/LayoutService.cs index 9cdf1acad..348b25fed 100644 --- a/Servers/SSFWServer/Services/SSFWLayoutService.cs +++ b/Servers/SSFWServer/Services/LayoutService.cs @@ -1,17 +1,16 @@ using CustomLogger; using Newtonsoft.Json.Linq; using SSFWServer.Helpers.FileHelper; +using SSFWServer.Helpers.RegexHelper; using System.Text; -using System.Text.RegularExpressions; - namespace SSFWServer.Services { - public partial class SSFWLayoutService + public class LayoutService { private string? key; - public SSFWLayoutService(string? key) + public LayoutService(string? key) { this.key = key; } @@ -46,7 +45,7 @@ public bool HandleLayoutServicePOST(byte[] buffer, string directorypath, string } else { - string scenename = scenemap.FirstOrDefault(x => x.Value == SSFWMisc.ExtractPortion(kvp.Key, 13, 18)).Key; + string scenename = scenemap.FirstOrDefault(x => x.Value == Program.ExtractPortion(kvp.Key, 13, 18)).Key; if (!string.IsNullOrEmpty(scenename)) { if (File.Exists(directorypath + $"/{kvp.Key}.json")) // SceneID now mapped, so SceneID based file has become obsolete. @@ -71,10 +70,8 @@ public bool HandleLayoutServicePOST(byte[] buffer, string directorypath, string { File.WriteAllText(directorypath + "/HarborStudio.json", Encoding.UTF8.GetString(buffer)); handled = true; - } - else - { - string scenename = scenemap.FirstOrDefault(x => x.Value == SSFWMisc.ExtractPortion(sceneid, 13, 18)).Key; + } else { + string scenename = scenemap.FirstOrDefault(x => x.Value == Program.ExtractPortion(sceneid, 13, 18)).Key; if (!string.IsNullOrEmpty(scenename)) { if (File.Exists(directorypath + $"/{sceneid}.json")) // SceneID now mapped, so SceneID based file has become obsolete. @@ -106,13 +103,7 @@ public bool HandleLayoutServicePOST(byte[] buffer, string directorypath, string if (words.Length > 0) sceneid = words[^1]; -#if NET7_0_OR_GREATER - Match match = UUIDRegex().Match(sceneid); -#else - Match match = new Regex(@"[0-9a-fA-F]{8}-[0-9a-fA-F]{8}-[0-9a-fA-F]{8}-[0-9a-fA-F]{8}").Match(sceneid); -#endif - - if (match.Success) // If is UUID Ok. + if (GUIDValidator.RegexSceneIdValidMatch(sceneid).Success) // If is UUID Ok. { if (File.Exists(SSFWServerConfiguration.ScenelistFile)) { @@ -123,7 +114,7 @@ public bool HandleLayoutServicePOST(byte[] buffer, string directorypath, string } else { - string scenename = ScenelistParser.sceneDictionary.FirstOrDefault(x => x.Value == SSFWMisc.ExtractPortion(sceneid, 13, 18)).Key; + string scenename = ScenelistParser.sceneDictionary.FirstOrDefault(x => x.Value == Program.ExtractPortion(sceneid, 13, 18)).Key; if (!string.IsNullOrEmpty(scenename)) { string filepath = directorypath + $"/{scenename}.json"; @@ -250,9 +241,5 @@ public Dictionary SSFWGetLegacyFurnitureLayouts(string filePath) return outputDictionary; } -#if NET7_0_OR_GREATER - [GeneratedRegex("[0-9a-fA-F]{8}-[0-9a-fA-F]{8}-[0-9a-fA-F]{8}-[0-9a-fA-F]{8}")] - private static partial Regex UUIDRegex(); -#endif } } diff --git a/Servers/SSFWServer/Services/PlayerLookupService.cs b/Servers/SSFWServer/Services/PlayerLookupService.cs new file mode 100644 index 000000000..a4a913659 --- /dev/null +++ b/Servers/SSFWServer/Services/PlayerLookupService.cs @@ -0,0 +1,17 @@ +using CustomLogger; + +namespace SSFWServer.Services +{ + public class PlayerLookupService + { + public string HandlePlayerLookupService(string url) + { + string byDisplayName = url.Split("=")[1]; + string? userId = SSFWUserSessionManager.GetIdByUsername(byDisplayName); +#if DEBUG + LoggerAccessor.LogInfo($"[SSFW] PlayerLookupService - Requesting {byDisplayName}'s id, successfully returned userId {userId}"); +#endif + return $"{{\"@id\": {userId} }}"; + } + } +} \ No newline at end of file diff --git a/Servers/SSFWServer/Services/SSFWRewardsService.cs b/Servers/SSFWServer/Services/RewardsService.cs similarity index 62% rename from Servers/SSFWServer/Services/SSFWRewardsService.cs rename to Servers/SSFWServer/Services/RewardsService.cs index 03799a6c0..8c46c8e6d 100644 --- a/Servers/SSFWServer/Services/SSFWRewardsService.cs +++ b/Servers/SSFWServer/Services/RewardsService.cs @@ -9,11 +9,11 @@ namespace SSFWServer.Services { - public class SSFWRewardsService + public class RewardsService { - private string? key; + private readonly string? key; - public SSFWRewardsService(string? key) + public RewardsService(string? key) { this.key = key; } @@ -29,11 +29,35 @@ public byte[] HandleRewardServicePOST(byte[] buffer, string directorypath, strin return buffer; } - public byte[] HandleRewardServiceInvPOST(byte[] buffer, string directorypath, string filepath, string absoultepath) + public byte[] HandleRewardServiceInvPOST(byte[] buffer, string directorypath, string filepath, string absolutepath) { Directory.CreateDirectory(directorypath); - return SSFWRewardServiceInventoryPOST(buffer, directorypath, filepath, absoultepath); + return RewardServiceInventory(buffer, directorypath, filepath, absolutepath, false, false); + } + + public byte[]? HandleRewardServiceInvCardTrackingDataDELETE(string directorypath, string filepath, string absolutepath, string userAgent, string sessionId) + { + AdminObjectService adminObjectService = new(sessionId, key); + if (adminObjectService.IsAdminVerified(userAgent)) + { + return RewardServiceInventory(Array.Empty(), directorypath, filepath, absolutepath, false, true); + } else { + LoggerAccessor.LogWarn($"[SSFW] - HandleRewardServiceInvCardTrackingDataDELETE : {SSFWUserSessionManager.GetIdBySessionId(sessionId)} Unauthorized to delete Card Tracking data!"); + return null; + } + } + + public byte[]? HandleRewardServiceWipeInvDELETE(string directorypath, string filepath, string absolutepath, string userAgent, string sessionId) + { + AdminObjectService adminObjectService = new(sessionId, key); + if(adminObjectService.IsAdminVerified(userAgent)) + { + return RewardServiceInventory(Array.Empty(), directorypath, filepath, absolutepath, true, false); + } else { + LoggerAccessor.LogWarn($"[SSFW] - HandleRewardServiceWipeInvDELETE : {SSFWUserSessionManager.GetIdBySessionId(sessionId)} Unauthorized to wipe inventory data!"); + return null; + } } public void HandleRewardServiceTrunksPOST(byte[] buffer, string directorypath, string filepath, string absolutepath, string env, string? userId) @@ -42,7 +66,7 @@ public void HandleRewardServiceTrunksPOST(byte[] buffer, string directorypath, s File.WriteAllBytes($"{SSFWServerConfiguration.SSFWStaticFolder}/{absolutepath}.json", buffer); - SSFWTrunkServiceProcess(filepath.Replace("/setpartial", string.Empty) + ".json", Encoding.UTF8.GetString(buffer), env, userId); + TrunkServiceProcess(filepath.Replace("/setpartial", string.Empty) + ".json", Encoding.UTF8.GetString(buffer), env, userId); } public void HandleRewardServiceTrunksEmergencyPOST(byte[] buffer, string directorypath, string absolutepath) @@ -77,7 +101,8 @@ public void SSFWUpdateMini(string filePath, string postData, bool delete) foreach (var reward in rewardsObject) { string rewardKey = reward.Key; - if (string.IsNullOrEmpty(rewardKey) || reward.Value == null) + JToken? rewardValue = reward.Value; + if (string.IsNullOrEmpty(rewardKey) || rewardValue == null) continue; // Check if the reward exists in the JSON array @@ -114,7 +139,7 @@ public void SSFWUpdateMini(string filePath, string postData, bool delete) } } - public void SSFWTrunkServiceProcess(string filePath, string request, string env, string? userId) + public void TrunkServiceProcess(string filePath, string request, string env, string? userId) { try { @@ -137,12 +162,12 @@ public void SSFWTrunkServiceProcess(string filePath, string request, string env, JArray? mainArray = (JArray?)mainFile["objects"]; if (mainArray != null) { - Dictionary entriesToAddInMini = new Dictionary(); + Dictionary entriesToAddInMini = new(); foreach (JObject addObject in addArray) { mainArray.Add(addObject); - if (addObject.TryGetValue("objectId", out JToken? objectIdToken) && objectIdToken != null + if (addObject.TryGetValue("objectId", out JToken? objectIdToken) && objectIdToken != null && addObject.TryGetValue("type", out JToken? typeToken) && typeToken != null && int.TryParse(typeToken.ToString(), out int typeTokenInt) && typeTokenInt != 0) entriesToAddInMini.TryAdd(objectIdToken.ToString(), typeToken.ToString()); } @@ -157,7 +182,7 @@ public void SSFWTrunkServiceProcess(string filePath, string request, string env, foreach (var entry in entriesToAddInMini) { - SSFWUpdateMini(miniPath, $"{{\"rewards\":{{\"{entry.Key}\": {entry.Value}}}}}", false); + SSFWUpdateMini(miniPath, $"{{ \"rewards\": {{ \"{entry.Key}\": {entry.Value} }} }}", false); } } } @@ -187,7 +212,7 @@ public void SSFWTrunkServiceProcess(string filePath, string request, string env, JArray? mainArray = (JArray?)mainFile["objects"]; if (mainArray != null) { - List entriesToRemoveInMini = new List(); + List entriesToRemoveInMini = new(); foreach (JObject deleteObj in deleteArray) { @@ -210,7 +235,7 @@ public void SSFWTrunkServiceProcess(string filePath, string request, string env, { foreach (string entry in entriesToRemoveInMini) { - SSFWUpdateMini(miniPath, $"{{\"rewards\":{{\"{entry}\": -1}}}}", true); + SSFWUpdateMini(miniPath, $"{{ \"rewards\": {{ \"{entry}\": -1 }} }}", true); } } } @@ -224,157 +249,198 @@ public void SSFWTrunkServiceProcess(string filePath, string request, string env, } catch (Exception ex) { - LoggerAccessor.LogError($"[SSFW] - SSFWTrunkServiceProcess errored out with this exception - {ex}"); + LoggerAccessor.LogError($"[SSFW] - TrunkServiceProcess errored out with this exception - {ex}"); } } - public byte[] SSFWRewardServiceInventoryPOST(byte[] buffer, string directorypath, string filepath, string absolutePath) + public static byte[] RewardServiceInventory(byte[] buffer, string directorypath, string filepath, string absolutePath, bool deleteInv, bool deleteOnlyTracking) { - //Tracking Inventory + //Tracking Inventory GUID const string trackingGuid = "00000000-00000000-00000000-00000001"; // fallback/hardcoded tracking GUID + //Only return trackingGuid on error + var errorPayload = Encoding.UTF8.GetBytes($"{{\"idList\": [\"{trackingGuid}\"] }}"); + // File paths based on the provided format - string countsStoreDir = $"{SSFWServerConfiguration.SSFWStaticFolder}/{absolutePath}/"; + string countsStoreDir = $"{SSFWServerConfiguration.SSFWStaticFolder}/{absolutePath}"; string countsStore = $"{countsStoreDir}/counts.json"; - string trackingFileDir = $"{SSFWServerConfiguration.SSFWStaticFolder}/{absolutePath}/object/"; + + string trackingFileDir = $"{SSFWServerConfiguration.SSFWStaticFolder}/{absolutePath}/object"; string trackingFile = $"{trackingFileDir}/{trackingGuid}.json"; - Directory.CreateDirectory(Path.GetDirectoryName(countsStoreDir)); - Directory.CreateDirectory(Path.GetDirectoryName(trackingFileDir)); + if (!string.IsNullOrEmpty(countsStoreDir) && !string.IsNullOrEmpty(trackingFileDir)) { + Directory.CreateDirectory(Path.GetDirectoryName(countsStoreDir)); + Directory.CreateDirectory(Path.GetDirectoryName(trackingFileDir)); + } + else + { + LoggerAccessor.LogError("[SSFW] - RewardServiceInventoryPOST: Fatal error in RewardService Inventory System! CountsStoreDir or TrackingFileDir should NOT be null!"); + return errorPayload; + } + //Parse Buffer string fixedJsonPayload = GUIDValidator.FixJsonValues(Encoding.UTF8.GetString(buffer)); try { using JsonDocument document = JsonDocument.Parse(fixedJsonPayload); JsonElement root = document.RootElement; + if (!root.TryGetProperty("rewards", out JsonElement rewardsElement) || rewardsElement.ValueKind != JsonValueKind.Array) { - LoggerAccessor.LogError("[SSFW] - SSFWRewardServiceInventoryPOST: Invalid payload - 'rewards' must be an array."); - return Encoding.UTF8.GetBytes("{\"idList\": [\"00000000-00000000-00000000-00000001\"] }"); + LoggerAccessor.LogError("[SSFW] - RewardServiceInventoryPOST: Invalid payload - 'rewards' must be an array."); + return errorPayload; } var rewards = rewardsElement.EnumerateArray(); if (!rewards.MoveNext()) { - LoggerAccessor.LogError("[SSFW] - SSFWRewardServiceInventoryPOST: Invalid payload - 'rewards' array is empty."); - return Encoding.UTF8.GetBytes("{\"idList\": [\"00000000-00000000-00000000-00000001\"] }"); + LoggerAccessor.LogError("[SSFW] - RewardServiceInventoryPOST: Invalid payload - 'rewards' array is empty."); + return errorPayload; } - Dictionary counts; + Dictionary counts = new(); if (File.Exists(countsStore)) { - string countsJson = File.ReadAllText(countsStore); - counts = System.Text.Json.JsonSerializer.Deserialize>(countsJson) ?? new Dictionary(); + if(deleteInv) + { + File.Delete(countsStore); +#if DEBUG + LoggerAccessor.LogInfo($"[SSFW] - RewardServiceInventory: Successfully deleted Inventory counts at {countsStore}"); +#endif + } else + { + string countsJson = File.ReadAllText(countsStore); + counts = System.Text.Json.JsonSerializer.Deserialize>(countsJson) ?? new Dictionary(); + } } else { counts = new Dictionary(); } - Dictionary> existingTrackingData = null; + Dictionary>? existingTrackingData = null; if (File.Exists(trackingFile)) { - string existingTrackingJson = File.ReadAllText(trackingFile); - using JsonDocument trackingDoc = JsonDocument.Parse(existingTrackingJson); - JsonElement trackingRoot = trackingDoc.RootElement; - if (trackingRoot.TryGetProperty("rewards", out JsonElement trackingRewardsElement) && - trackingRewardsElement.ValueKind == JsonValueKind.Object) + if (deleteInv || deleteOnlyTracking) { - existingTrackingData = System.Text.Json.JsonSerializer.Deserialize>>( - trackingRewardsElement.GetRawText()); - } - } - - foreach (JsonElement reward in rewards) - { - if (!reward.TryGetProperty("objectId", out JsonElement objectIdElement) || - objectIdElement.ValueKind != JsonValueKind.String) - { - LoggerAccessor.LogError("[SSFW] - SSFWRewardServiceInventoryPOST: Invalid reward - 'objectId' missing or not a string."); - continue; + File.Delete(trackingFile); +#if DEBUG + LoggerAccessor.LogInfo($"[SSFW] - RewardServiceInventory: Deleting Tracking file at {trackingFile}"); +#endif + return Encoding.UTF8.GetBytes(""); } - - if (objectIdElement.ValueKind != JsonValueKind.String) + else { - LoggerAccessor.LogError($"[SSFW] - SSFWRewardServiceInventoryPOST: 'objectId' must be a string, got {objectIdElement.ValueKind}."); - continue; - } + string existingTrackingJson = File.ReadAllText(trackingFile); + using JsonDocument trackingDoc = JsonDocument.Parse(existingTrackingJson); + JsonElement trackingRoot = trackingDoc.RootElement; + if (trackingRoot.TryGetProperty("rewards", out JsonElement trackingRewardsElement) && + trackingRewardsElement.ValueKind == JsonValueKind.Object) + { + existingTrackingData = System.Text.Json.JsonSerializer.Deserialize>>( + trackingRewardsElement.GetRawText()); + } - string objectId = objectIdElement.GetString(); + foreach (JsonElement reward in rewards) + { + if (!reward.TryGetProperty("objectId", out JsonElement objectIdElement) || + objectIdElement.ValueKind != JsonValueKind.String) + { + LoggerAccessor.LogError("[SSFW] - RewardServiceInventoryPOST: Invalid reward - 'objectId' missing or not a string."); + continue; + } - // Update counts - if (counts.ContainsKey(objectId)) - { - counts[objectId]++; - } - else - { - counts[objectId] = 1; - } + if (objectIdElement.ValueKind != JsonValueKind.String) + { + LoggerAccessor.LogError($"[SSFW] - RewardServiceInventoryPOST: 'objectId' must be a string, got {objectIdElement.ValueKind}."); + continue; + } - // Check if this is a tracking object (has metadata or matches tracking GUID) - bool hasMetadata = reward.TryGetProperty("_id", out _) || - reward.TryGetProperty("scene", out _) || - reward.TryGetProperty("boost", out _) || - reward.TryGetProperty("game", out _) || - reward.TryGetProperty("migrated", out _); - if (hasMetadata || objectId == trackingGuid || objectId != string.Empty) - { - var trackingRewards = existingTrackingData ?? new Dictionary>(); - var metadata = new Dictionary(); + string? objectId = objectIdElement.GetString(); + if (!string.IsNullOrEmpty(objectId)) + { + // Update counts + if (counts.ContainsKey(objectId)) + { + counts[objectId]++; + } + else + { + counts[objectId] = 1; + } + } - foreach (JsonProperty prop in reward.EnumerateObject()) - { - if (prop.Name != "objectId") + // Check if this is a tracking object (has metadata or matches tracking GUID) + bool hasMetadata = reward.TryGetProperty("_id", out _) || + reward.TryGetProperty("scene", out _) || + reward.TryGetProperty("boost", out _) || + reward.TryGetProperty("game", out _) || + reward.TryGetProperty("migrated", out _); + if (hasMetadata || objectId == trackingGuid || objectId != string.Empty) { - switch (prop.Value.ValueKind) + var trackingRewards = existingTrackingData ?? new Dictionary>(); + var metadata = new Dictionary(); + + foreach (JsonProperty prop in reward.EnumerateObject()) { - case JsonValueKind.String: - metadata[prop.Name] = prop.Value.GetString(); - break; - case JsonValueKind.Number: - metadata[prop.Name] = prop.Value.GetInt32(); - break; - case JsonValueKind.True: - case JsonValueKind.False: - metadata[prop.Name] = prop.Value.GetBoolean(); - break; - default: - metadata[prop.Name] = prop.Value.ToString(); - break; + if (prop.Name != "objectId") + { + switch (prop.Value.ValueKind) + { + case JsonValueKind.String: + metadata[prop.Name] = prop.Value.GetString() ?? ""; + break; + case JsonValueKind.Number: + metadata[prop.Name] = prop.Value.GetInt32(); + break; + case JsonValueKind.True: + case JsonValueKind.False: + metadata[prop.Name] = prop.Value.GetBoolean(); + break; + default: + metadata[prop.Name] = prop.Value.ToString(); + break; + } + } } + + if (!string.IsNullOrEmpty(objectId)) + { + trackingRewards[objectId] = metadata; + } + + // Write tracking data + var trackingData = new Dictionary + { + { "result", 0 }, + { "rewards", trackingRewards } + }; + string trackingJson = System.Text.Json.JsonSerializer.Serialize(trackingData, new JsonSerializerOptions { WriteIndented = true }); + File.WriteAllText(trackingFile, trackingJson); +#if DEBUG + LoggerAccessor.LogInfo($"[SSFW] - RewardServiceInventoryPOST: Updated tracking file: {trackingFile}"); +#endif } } - trackingRewards[objectId] = metadata; - - // Write tracking data - var trackingData = new Dictionary - { - { "result", 0 }, - { "rewards", trackingRewards } - }; - string trackingJson = System.Text.Json.JsonSerializer.Serialize(trackingData, new JsonSerializerOptions { WriteIndented = true }); - File.WriteAllText(trackingFile, trackingJson); + string updatedCountsJson = System.Text.Json.JsonSerializer.Serialize(counts, new JsonSerializerOptions { WriteIndented = true }); + File.WriteAllText(countsStore, updatedCountsJson); #if DEBUG - LoggerAccessor.LogInfo($"[SSFW] - SSFWRewardServiceInventoryPOST: Updated tracking file: {trackingFile}"); + LoggerAccessor.LogInfo($"[SSFW] - RewardServiceInventoryPOST: Updated counts file: {countsStore}"); #endif } + } - string updatedCountsJson = System.Text.Json.JsonSerializer.Serialize(counts, new JsonSerializerOptions { WriteIndented = true }); - File.WriteAllText(countsStore, updatedCountsJson); -#if DEBUG - LoggerAccessor.LogInfo($"[SSFW] - SSFWRewardServiceInventoryPOST: Updated counts file: {countsStore}"); -#endif + } catch (System.Text.Json.JsonException ex) { - LoggerAccessor.LogError($"[SSFW] - SSFWRewardServiceInventoryPOST: Error parsing JSON payload: {ex.Message}"); + LoggerAccessor.LogError($"[SSFW] - RewardServiceInventoryPOST: Error parsing JSON payload: {ex.Message}"); } catch (Exception ex) { - LoggerAccessor.LogError($"[SSFW] - SSFWRewardServiceInventoryPOST: Error processing POST request: {ex.Message}"); + LoggerAccessor.LogError($"[SSFW] - RewardServiceInventoryPOST: Error processing POST request: {ex.Message}"); } return Encoding.UTF8.GetBytes(@"{ ""idList"": [""00000000-00000000-00000000-00000001""]}"); @@ -449,7 +515,7 @@ private void ProcessTrunkObjectUpdate(string trunkFilePath, Dictionary entries, int startIndex) + private static string BuildAddSetPartialJson(Dictionary entries, int startIndex) { // Create the object to build the JSON structure var jsonObject = new @@ -487,7 +553,7 @@ private string BuildAddSetPartialJson(Dictionary entries, int star return JsonConvert.SerializeObject(jsonObject); } - private string BuildDeleteSetPartialJson(Dictionary entries, Dictionary indexToItem) + private static string BuildDeleteSetPartialJson(Dictionary entries, Dictionary indexToItem) { // Create the object to build the JSON structure var jsonObject = new @@ -514,4 +580,4 @@ private string BuildDeleteSetPartialJson(Dictionary entries, Dicti return JsonConvert.SerializeObject(jsonObject); } } -} +} \ No newline at end of file diff --git a/Servers/SSFWServer/Services/SSFWAuditService.cs b/Servers/SSFWServer/Services/SSFWAuditService.cs deleted file mode 100644 index f826dd476..000000000 --- a/Servers/SSFWServer/Services/SSFWAuditService.cs +++ /dev/null @@ -1,41 +0,0 @@ -using CastleLibrary.Sony.SSFW; -using CustomLogger; -using Newtonsoft.Json; -using System.Text; - -namespace SSFWServer.Services -{ - public class SSFWAuditService - { - private string? sessionid; - private string? env; - private string? key; - - public SSFWAuditService(string sessionid, string env, string? key) - { - this.sessionid = sessionid; - this.env = env; - this.key = key; - } - - public void HandleAuditService(string absolutepath, byte[] buffer) - { - string fileNameGUID = GuidGenerator.SSFWGenerateGuid(sessionid, env); - string auditLogPath = $"{SSFWServerConfiguration.SSFWStaticFolder}/{absolutepath}/{env}"; - try - { - Directory.CreateDirectory(auditLogPath); - - var obj = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(buffer)); - File.WriteAllText($"{auditLogPath}/{fileNameGUID}.json", JsonConvert.SerializeObject(obj, Formatting.Indented)); -#if DEBUG - LoggerAccessor.LogInfo($"[SSFW] : Audit event posted: {fileNameGUID}"); -#endif - } - catch (Exception ex) - { - LoggerAccessor.LogError($"[SSFW] - SSFWAuditService HandleAuditService ERROR: \n{ex}"); - } - } - } -} \ No newline at end of file diff --git a/Servers/SSFWServer/Services/SSFWClanService.cs b/Servers/SSFWServer/Services/SSFWClanService.cs deleted file mode 100644 index 80162d686..000000000 --- a/Servers/SSFWServer/Services/SSFWClanService.cs +++ /dev/null @@ -1,87 +0,0 @@ -using System.Security.Cryptography; -using System.Text; -using System.Text.Json; -using NetCoreServer; -using NetHasher; - -namespace SSFWServer.Services -{ - public class SSFWClanService - { - private readonly string? _sessionid; - - public SSFWClanService(string sessionid) - { - _sessionid = sessionid; - } - - // Handles both GET/POST on this - public HttpResponse HandleClanDetailsService(HttpRequest req, HttpResponse res, string absolutepath) - { - string filePath = $"{SSFWServerConfiguration.SSFWStaticFolder}{new Uri(absolutepath).AbsolutePath}" + ".json"; - - if (req.Method == HttpMethod.Post.ToString()) - { - try - { - using JsonDocument doc = JsonDocument.Parse(req.Body); - - if (doc.RootElement.TryGetProperty("sceneObjectId", out JsonElement idElement)) - { - Directory.CreateDirectory(absolutepath); - - string jsonToWrite = $@"{{ - ""region"":""en-US"", - ""message"":""OK"", - ""result"":0, - ""psnClanId"":{absolutepath.Split("/").LastOrDefault()}, - ""sceneObjectId"":""{idElement.GetString()!}"", - ""personId"":""{_sessionid}"", - ""clanId"":""{DotNetHasher.ComputeMD5String(Encoding.UTF8.GetBytes(absolutepath.Split("/").LastOrDefault()!))}"" - }}"; - - File.WriteAllText(filePath, jsonToWrite); - - return res.MakeGetResponse($@"{jsonToWrite}", "application/json"); - } - - } - catch - { - // Not Important. - } - - return res.MakeErrorResponse(400); - } - else if (req.Method == HttpMethod.Get.ToString()) // GET ONLY - { - // If clanid exist, we check json and return that back, otherwise not found so Home POST default - if (File.Exists(filePath)) - return res.MakeGetResponse($@"{File.ReadAllText(filePath)}", "application/json"); - - return res.MakeErrorResponse(404, "Not Found"); - } - - // Delete clan details - try - { - if (File.Exists(filePath)) - { - using JsonDocument doc = JsonDocument.Parse(File.ReadAllText(filePath)); - - string? sceneObjectId = doc.RootElement.GetProperty("sceneObjectId").GetString() ?? string.Empty; - - File.Delete(filePath); - - return res.MakeGetResponse($"{{\"sceneObjectIds\":[\"{sceneObjectId}\"]}}", "application/json"); - } - } - catch - { - // Not Important. - } - - return res.MakeErrorResponse(); - } - } -} diff --git a/Servers/SSFWServer/Helpers/SaveDataHelper/SSFWGetFileList.cs b/Servers/SSFWServer/Services/SaveDataService.cs similarity index 78% rename from Servers/SSFWServer/Helpers/SaveDataHelper/SSFWGetFileList.cs rename to Servers/SSFWServer/Services/SaveDataService.cs index 438de9734..bcef7ac0c 100644 --- a/Servers/SSFWServer/Helpers/SaveDataHelper/SSFWGetFileList.cs +++ b/Servers/SSFWServer/Services/SaveDataService.cs @@ -1,11 +1,12 @@ -using CustomLogger; -using Newtonsoft.Json; +using CustomLogger; +using System.Text.Json; +using System.Text.RegularExpressions; -namespace SSFWServer.SaveDataHelper +namespace SSFWServer.Services { - public static class SSFWGetFileList + public class SaveDataService { - public static string? SSFWSaveDataDebugGetFileList(string directoryPath, string? segment) + public string? DebugGetFileList(string directoryPath, string? segment) { try { @@ -14,12 +15,12 @@ public static class SSFWGetFileList List? files = GetFilesInfo(directoryPath + "/" + segment); if (files != null) - return JsonConvert.SerializeObject(new FilesContainer() { files = files }, Formatting.Indented); + return JsonSerializer.Serialize(new FilesContainer() { files = files }); } } catch (Exception e) { - LoggerAccessor.LogError($"[SSFW] - SaveDataDebug GetFileList ERROR: \n{e}"); + LoggerAccessor.LogError($"[SSFW] - DebugGetFileList ERROR: \n{e}"); } return null; diff --git a/Servers/SSFWServer/Services/TradingService.cs b/Servers/SSFWServer/Services/TradingService.cs new file mode 100644 index 000000000..7ea613da1 --- /dev/null +++ b/Servers/SSFWServer/Services/TradingService.cs @@ -0,0 +1,288 @@ +using CustomLogger; +using NetCoreServer; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace SSFWServer.Services +{ + public class TradingService + { + private readonly string? sessionid; + private readonly string? env; + private readonly string? key; + + public TradingService(string sessionid, string env, string? key) + { + this.sessionid = sessionid; + this.env = env; + this.key = key; + } + + public class BaseResponse + { + [JsonProperty(nameof(result))] + public int result { get; set; } = -1; + [JsonProperty(nameof(id))] + public int id { get; set; } = -1; + [JsonProperty(nameof(message))] + public string message { get; set; } = string.Empty; + } + + public class RootObject + { + [JsonProperty(nameof(members))] + public List? members { get; set; } + } + + private static List tradeTransactions = new(); + public class TradeTransactionResponse : BaseResponse + { + [JsonProperty(nameof(ownerId))] + public string ownerId { get; set; } = string.Empty; + [JsonProperty(nameof(joinerId))] + public string joinerId { get; set; } = string.Empty; + [JsonProperty(nameof(transactionId))] + public int transactionId { get; set; } = 0; + [JsonProperty(nameof(sequence))] + public long sequence { get; set; } = 0; + + public int tradeAmount { get; set; } = 0; + //itemList is a dictionary list of pairs. + public Dictionary tradeRequesterItemList { get; set; } = new Dictionary(); + public Dictionary tradePartnerItemList { get; set; } = new Dictionary(); + + public Status status { get; set; } + } + + public enum Status : int + { + Active = 0, + Commited = 1, + PartiallyCommited = 2, + Cancelled = 3 + } + + + public string HandleTradingService(HttpRequest req, string sessionid, string absolutepath) + { + BaseResponse tradeResponse = new(); + TradeTransactionResponse newTradeTransactionResponse = new(); + + int existingCardTradingTransactionId = 0; + int sequenceNum = 0; + var absoPathArray = absolutepath.Split("/"); + + string? currentUserId = SSFWUserSessionManager.GetIdBySessionId(sessionid); + if (string.IsNullOrEmpty(currentUserId)) + return JsonConvert.SerializeObject(tradeResponse); +#if DEBUG + LoggerAccessor.LogInfo(absoPathArray.Length); +#endif + //if this is a existing Trade Transaction, assign! + if (absoPathArray.Length > 3) + { + existingCardTradingTransactionId = Convert.ToInt32(absolutepath.Split("/")[3]); + } + + //CommitTrade sends SequenceNumber + if (absoPathArray.Length > 4) + { + sequenceNum = Convert.ToInt32(absolutepath.Split("/")[4]); + } + + if (req.Method == "POST") + { + //If we DO have a existing trade transaction in the process, handle it! + if (existingCardTradingTransactionId > 0) { + + foreach (var transaction in tradeTransactions) + { + //ADDTRADEITEMS + //If a existing transaction was created, update it here! + if (transaction.transactionId == existingCardTradingTransactionId) + { + transaction.transactionId = existingCardTradingTransactionId; + transaction.sequence = sequenceNum; //Set to 0 initially till set later + + try + { + // Deserialize directly into Dictionary using Newtonsoft.Json + var reqitemList = JsonConvert.DeserializeObject>(req.Body); + + if (reqitemList == null || reqitemList.Count == 0) + { + LoggerAccessor.LogInfo($"[SSFW] TradingService - Existing transaction {transaction.transactionId} failed to update, request contained no Items to add!"); + return JsonConvert.SerializeObject(tradeResponse); + } + + if(transaction.joinerId == currentUserId) + { + transaction.tradePartnerItemList = reqitemList; + } else //transaction.tradeRequester == curentUserId + { + transaction.tradeRequesterItemList = reqitemList; + } + + LoggerAccessor.LogInfo($"[SSFW] TradingService - Existing transaction {transaction.transactionId} has been updated"); + tradeTransactions.Add(transaction); + + tradeResponse.result = 0; + return JsonConvert.SerializeObject(tradeResponse); + } catch (Exception ex) + { + LoggerAccessor.LogError($"[SSFW] TradingService - Exception caught attempting to remove existing trade transaction id {existingCardTradingTransactionId} with error {ex}"); + + tradeResponse.result = -1; + return JsonConvert.SerializeObject(tradeResponse); + } + + } + } + + } else // otherwise create new transaction! + { + RootObject? result = JsonConvert.DeserializeObject(req.Body); + string memberValue = string.Empty; + if (result != null && result.members != null) + { + memberValue = result.members[0]; + } else { + return JsonConvert.SerializeObject(tradeResponse); + } + + int index = 1; + foreach (var transaction in tradeTransactions) + { + newTradeTransactionResponse.ownerId = currentUserId; + newTradeTransactionResponse.joinerId = memberValue; + + //If a existing transaction was created, update it here! + if (transaction.transactionId == existingCardTradingTransactionId) + { + //index = transaction.transId + 1; + + //newTradeRequest.transId = index; + //tradeTransactions.Add(newTradeRequest); + } + //Initial first Transaction starts at index 1 + else if (tradeTransactions.Count == 0) + { + newTradeTransactionResponse.transactionId = index; + newTradeTransactionResponse.status = Status.Active; + tradeTransactions.Add(newTradeTransactionResponse); + + } + } + + tradeResponse.result = 0; + tradeResponse.id = index; + return JsonConvert.SerializeObject(tradeResponse); + } + } + else if (req.Method == "GET") + { + #region Request Trade Status - Potentially unused + //Return current status of transaction + if (req.Url.Contains("status")) + { + var existingTrade = tradeTransactions.FirstOrDefault(x => x.transactionId == existingCardTradingTransactionId); + + if (existingTrade != null) + { + LoggerAccessor.LogInfo($"[SSFW] TradingService - Checking current status of transactionId {existingTrade.transactionId} between Requester {existingTrade.ownerId} & Partner {existingTrade.joinerId}: {existingTrade.status}"); + + //RootObject? result = JsonConvert.DeserializeObject(CreateTransactionBody); + + JObject jsonResponse = JObject.FromObject(existingTrade); + + jsonResponse[newTradeTransactionResponse.result] = 0; + jsonResponse[newTradeTransactionResponse.message] = "Success"; + jsonResponse[newTradeTransactionResponse.sequence] = existingTrade.sequence; + jsonResponse[newTradeTransactionResponse.ownerId] = existingTrade.ownerId; + jsonResponse[newTradeTransactionResponse.joinerId] = existingTrade.joinerId; + jsonResponse[newTradeTransactionResponse.status] = Convert.ToInt32(existingTrade.status); + + return jsonResponse.ToString(Formatting.Indented); + + /* Original + return $@" {{ + ""result"" : 0, + ""message"" : ""Success"", + ""sequence"" : {existingTrade.sequence}, + ""ownerId"" :""{existingTrade.tradeRequester}"", + ""joinerId"" :""{existingTrade.joinerId}"" + ""status"" : {existingTrade.status}"" + }}";*/ + + } + else + { + LoggerAccessor.LogInfo($"[SSFW] TradingService - GET method failed to find existing trade transaction!"); + return JsonConvert.SerializeObject(tradeResponse); + } + } else + { + + if(existingCardTradingTransactionId > 0) + { + foreach (var transaction in tradeTransactions) + { + //ADDTRADEITEMS + //If a existing transaction was created, update it here! + if (transaction.transactionId == existingCardTradingTransactionId) + { + return $@"{{ + ""result"": 0, + ""sequence"": {transaction.sequence}, + ""{transaction.ownerId}"": {{ +{string.Join("", transaction.tradeRequesterItemList)} + }}, + ""{transaction.joinerId}"": {{ +{string.Join("", transaction.tradePartnerItemList)} + }} +}}"; + } + } + + } + + } + #endregion + } + #region Cancel Trade Transaction + else if (req.Method == "DELETE") + { + if (tradeTransactions.Count > 0) + { + try + { + TradeTransactionResponse? tradeRemovalResp = tradeTransactions.FirstOrDefault(x => x.transactionId == existingCardTradingTransactionId) ?? null; + if (tradeRemovalResp != null) + { + tradeTransactions.Remove(tradeRemovalResp); + tradeResponse.result = 0; + + LoggerAccessor.LogError($"[SSFW] TradingService - Successfully cancelled existing trade transaction id {existingCardTradingTransactionId}"); + return JsonConvert.SerializeObject(tradeResponse); + } + else + { + LoggerAccessor.LogError($"[SSFW] TradingService - Unable to determine existing trade transaction to delete!"); + } + + } + catch (Exception e) { + LoggerAccessor.LogError($"[SSFW] TradingService - Exception caught attempting to remove existing trade transaction id {existingCardTradingTransactionId} with error {e}"); + return JsonConvert.SerializeObject(tradeResponse); + } + + } + + } + #endregion + + tradeResponse.result = 0; + return JsonConvert.SerializeObject(tradeResponse); + } + } +} \ No newline at end of file diff --git a/Servers/SSFWServer/static/layouts/HarborStudio.json b/Servers/SSFWServer/static/layouts/HarborStudio.json new file mode 100644 index 000000000..f54d70d61 --- /dev/null +++ b/Servers/SSFWServer/static/layouts/HarborStudio.json @@ -0,0 +1,204 @@ +{ + "version": 3, + "wallpaper": 2, + "furniture": [ + { + "flags": 0, + "furnitureObjectId": "00000000-00000000-00000002-00000010", + "instanceId": "4874595585", + "itemId": 0, + "positionX": -4.287144660949707, + "positionY": 2.999958038330078, + "positionZ": -2.3795166015625, + "rotationX": 2.6903744583250955E-06, + "rotationY": 0.7076740264892578, + "rotationZ": -2.1571504476014525E-06, + "rotationW": 0.7065391540527344, + "time": 1686384673 + }, + { + "flags": 0, + "furnitureObjectId": "00000000-00000000-00000002-00000002", + "instanceId": "4874595586", + "itemId": 1, + "positionX": -3.7360246181488037, + "positionY": 2.999990224838257, + "positionZ": -0.9341824650764465, + "rotationX": 1.5251726836140733E-05, + "rotationY": 0.9201474785804749, + "rotationZ": -0.00032892703893594444, + "rotationW": 0.39157184958457947, + "time": 1686384699 + }, + { + "flags": 0, + "furnitureObjectId": "00000000-00000000-00000002-00000002", + "instanceId": "4874595587", + "itemId": 2, + "positionX": -4.276202201843262, + "positionY": 2.9999568462371826, + "positionZ": -4.152399063110352, + "rotationX": 1.4554960570123399E-09, + "rotationY": 0.4747755229473114, + "rotationZ": -1.4769816480963982E-08, + "rotationW": 0.8801069259643555, + "time": 1686384723 + }, + { + "flags": 0, + "furnitureObjectId": "00000000-00000000-00000002-00000002", + "instanceId": "4874595588", + "itemId": 3, + "positionX": -2.8646721839904785, + "positionY": 2.9999570846557617, + "positionZ": -3.0560495853424072, + "rotationX": 0.00010053320875158533, + "rotationY": -0.2633626163005829, + "rotationZ": -3.858909985865466E-05, + "rotationW": 0.9646968841552734, + "time": 1686384751 + }, + { + "flags": 0, + "furnitureObjectId": "00000000-00000000-00000002-00000001", + "instanceId": "4874595589", + "itemId": 4, + "positionX": 3.9096813201904297, + "positionY": 2.999513626098633, + "positionZ": -4.281363010406494, + "rotationX": 4.328743307269178E-05, + "rotationY": -0.5309971570968628, + "rotationZ": -3.918715083273128E-05, + "rotationW": 0.8473736047744751, + "time": 1686384774 + }, + { + "flags": 0, + "furnitureObjectId": "00000000-00000000-00000002-00000004", + "instanceId": "4874595590", + "itemId": 5, + "positionX": 1.8418744802474976, + "positionY": 3.000164747238159, + "positionZ": -3.2746503353118896, + "rotationX": -5.499047620105557E-05, + "rotationY": -0.5317798256874084, + "rotationZ": -1.335094293608563E-05, + "rotationW": 0.8468826413154602, + "time": 1686384795 + }, + { + "flags": 0, + "furnitureObjectId": "00000000-00000000-00000002-00000008", + "instanceId": "4874595591", + "itemId": 6, + "positionX": 3.472640037536621, + "positionY": 3.0000433921813965, + "positionZ": 4.783566951751709, + "rotationX": 6.134732393547893E-05, + "rotationY": 0.9999926090240479, + "rotationZ": -1.7070769899873994E-05, + "rotationW": 0.0038405421655625105, + "time": 1686384822 + }, + { + "flags": 0, + "furnitureObjectId": "00000000-00000000-00000002-00000008", + "instanceId": "4874595592", + "itemId": 7, + "positionX": 3.4952659606933594, + "positionY": 3.0000007152557373, + "positionZ": 0.2776024341583252, + "rotationX": -1.2929040167364292E-05, + "rotationY": -0.0061355167999863625, + "rotationZ": -4.4378830352798104E-05, + "rotationW": 0.999981164932251, + "time": 1686384834 + }, + { + "flags": 0, + "furnitureObjectId": "00000000-00000000-00000002-00000001", + "instanceId": "4874595593", + "itemId": 8, + "positionX": 1.3067165613174438, + "positionY": 2.9994897842407227, + "positionZ": 2.546649694442749, + "rotationX": 2.845195740519557E-05, + "rotationY": 0.7056202292442322, + "rotationZ": -8.082762178673875E-06, + "rotationW": 0.7085902690887451, + "time": 1686384862 + }, + { + "flags": 0, + "furnitureObjectId": "00000000-00000000-00000002-00000003", + "instanceId": "4874595594", + "itemId": 9, + "positionX": 3.480368137359619, + "positionY": 2.9999568462371826, + "positionZ": 2.538585662841797, + "rotationX": 3.1659130428352E-08, + "rotationY": -0.7071276307106018, + "rotationZ": 8.144282048760942E-08, + "rotationW": 0.7070858478546143, + "time": 1686384884 + }, + { + "flags": 0, + "furnitureObjectId": "00000000-00000000-00000002-00000009", + "instanceId": "4874595595", + "itemId": 10, + "positionX": -3.5043892860412598, + "positionY": 2.9999568462371826, + "positionZ": -9.527653694152832, + "rotationX": -1.7184934222314041E-06, + "rotationY": 0.00023035785125102848, + "rotationZ": 2.522783972835896E-07, + "rotationW": 0.9999999403953552, + "time": 1686384912 + }, + { + "flags": 0, + "furnitureObjectId": "00000000-00000000-00000002-00000009", + "instanceId": "4874595596", + "itemId": 11, + "positionX": 3.6248698234558105, + "positionY": 2.9999566078186035, + "positionZ": -9.534708976745605, + "rotationX": -2.132455847458914E-07, + "rotationY": 2.0361580027383752E-05, + "rotationZ": -4.782236828759778E-08, + "rotationW": 1, + "time": 1686384931 + }, + { + "flags": 0, + "furnitureObjectId": "00000000-00000000-00000002-00000005", + "instanceId": "4874595597", + "itemId": 12, + "positionX": -3.506892681121826, + "positionY": 3.488347291946411, + "positionZ": -9.531390190124512, + "rotationX": -0.0009180115885101259, + "rotationY": 0.006055513396859169, + "rotationZ": 0.000585820700507611, + "rotationW": 0.9999810457229614, + "time": 1686384961, + "photo": "/Furniture/Modern2/lampOutputcube.dds" + }, + { + "flags": 0, + "furnitureObjectId": "00000000-00000000-00000002-00000005", + "instanceId": "4874595598", + "itemId": 13, + "positionX": 3.617129325866699, + "positionY": 3.4891724586486816, + "positionZ": -9.53490161895752, + "rotationX": 0.00042979296995326877, + "rotationY": -0.009252170100808144, + "rotationZ": -0.00027207753737457097, + "rotationW": 0.9999570250511169, + "time": 1686385008, + "photo": "/Furniture/Modern2/lampOutputcube.dds" + } + ] +} \ No newline at end of file diff --git a/Servers/SSFWServer/static/layouts/LegacyLayout.json b/Servers/SSFWServer/static/layouts/LegacyLayout.json new file mode 100644 index 000000000..01688f728 --- /dev/null +++ b/Servers/SSFWServer/static/layouts/LegacyLayout.json @@ -0,0 +1,208 @@ +[ + { + "00000000-00000000-00000000-00000004": { + "version": 3, + "wallpaper": 2, + "furniture": [ + { + "flags": 0, + "furnitureObjectId": "00000000-00000000-00000002-00000010", + "instanceId": "4874595585", + "itemId": 0, + "positionX": -4.287144660949707, + "positionY": 2.9999580383300781, + "positionZ": -2.3795166015625, + "rotationX": 2.6903744583250955E-06, + "rotationY": 0.70767402648925781, + "rotationZ": -2.1571504476014525E-06, + "rotationW": 0.70653915405273438, + "time": 1686384673 + }, + { + "flags": 0, + "furnitureObjectId": "00000000-00000000-00000002-00000002", + "instanceId": "4874595586", + "itemId": 1, + "positionX": -3.7360246181488037, + "positionY": 2.9999902248382568, + "positionZ": -0.93418246507644653, + "rotationX": 1.5251726836140733E-05, + "rotationY": 0.92014747858047485, + "rotationZ": -0.00032892703893594444, + "rotationW": 0.39157184958457947, + "time": 1686384699 + }, + { + "flags": 0, + "furnitureObjectId": "00000000-00000000-00000002-00000002", + "instanceId": "4874595587", + "itemId": 2, + "positionX": -4.2762022018432617, + "positionY": 2.9999568462371826, + "positionZ": -4.1523990631103516, + "rotationX": 1.4554960570123399E-09, + "rotationY": 0.4747755229473114, + "rotationZ": -1.4769816480963982E-08, + "rotationW": 0.88010692596435547, + "time": 1686384723 + }, + { + "flags": 0, + "furnitureObjectId": "00000000-00000000-00000002-00000002", + "instanceId": "4874595588", + "itemId": 3, + "positionX": -2.8646721839904785, + "positionY": 2.9999570846557617, + "positionZ": -3.0560495853424072, + "rotationX": 0.00010053320875158533, + "rotationY": -0.26336261630058289, + "rotationZ": -3.8589099858654663E-05, + "rotationW": 0.96469688415527344, + "time": 1686384751 + }, + { + "flags": 0, + "furnitureObjectId": "00000000-00000000-00000002-00000001", + "instanceId": "4874595589", + "itemId": 4, + "positionX": 3.9096813201904297, + "positionY": 2.9995136260986328, + "positionZ": -4.2813630104064941, + "rotationX": 4.3287433072691783E-05, + "rotationY": -0.53099715709686279, + "rotationZ": -3.9187150832731277E-05, + "rotationW": 0.8473736047744751, + "time": 1686384774 + }, + { + "flags": 0, + "furnitureObjectId": "00000000-00000000-00000002-00000004", + "instanceId": "4874595590", + "itemId": 5, + "positionX": 1.8418744802474976, + "positionY": 3.0001647472381592, + "positionZ": -3.2746503353118896, + "rotationX": -5.4990476201055571E-05, + "rotationY": -0.53177982568740845, + "rotationZ": -1.335094293608563E-05, + "rotationW": 0.84688264131546021, + "time": 1686384795 + }, + { + "flags": 0, + "furnitureObjectId": "00000000-00000000-00000002-00000008", + "instanceId": "4874595591", + "itemId": 6, + "positionX": 3.4726400375366211, + "positionY": 3.0000433921813965, + "positionZ": 4.783566951751709, + "rotationX": 6.1347323935478926E-05, + "rotationY": 0.99999260902404785, + "rotationZ": -1.7070769899873994E-05, + "rotationW": 0.0038405421655625105, + "time": 1686384822 + }, + { + "flags": 0, + "furnitureObjectId": "00000000-00000000-00000002-00000008", + "instanceId": "4874595592", + "itemId": 7, + "positionX": 3.4952659606933594, + "positionY": 3.0000007152557373, + "positionZ": 0.2776024341583252, + "rotationX": -1.2929040167364292E-05, + "rotationY": -0.0061355167999863625, + "rotationZ": -4.4378830352798104E-05, + "rotationW": 0.999981164932251, + "time": 1686384834 + }, + { + "flags": 0, + "furnitureObjectId": "00000000-00000000-00000002-00000001", + "instanceId": "4874595593", + "itemId": 8, + "positionX": 1.3067165613174438, + "positionY": 2.9994897842407227, + "positionZ": 2.546649694442749, + "rotationX": 2.8451957405195571E-05, + "rotationY": 0.70562022924423218, + "rotationZ": -8.0827621786738746E-06, + "rotationW": 0.70859026908874512, + "time": 1686384862 + }, + { + "flags": 0, + "furnitureObjectId": "00000000-00000000-00000002-00000003", + "instanceId": "4874595594", + "itemId": 9, + "positionX": 3.4803681373596191, + "positionY": 2.9999568462371826, + "positionZ": 2.5385856628417969, + "rotationX": 3.1659130428352E-08, + "rotationY": -0.70712763071060181, + "rotationZ": 8.1442820487609424E-08, + "rotationW": 0.70708584785461426, + "time": 1686384884 + }, + { + "flags": 0, + "furnitureObjectId": "00000000-00000000-00000002-00000009", + "instanceId": "4874595595", + "itemId": 10, + "positionX": -3.5043892860412598, + "positionY": 2.9999568462371826, + "positionZ": -9.527653694152832, + "rotationX": -1.7184934222314041E-06, + "rotationY": 0.00023035785125102848, + "rotationZ": 2.5227839728358958E-07, + "rotationW": 0.99999994039535522, + "time": 1686384912 + }, + { + "flags": 0, + "furnitureObjectId": "00000000-00000000-00000002-00000009", + "instanceId": "4874595596", + "itemId": 11, + "positionX": 3.6248698234558105, + "positionY": 2.9999566078186035, + "positionZ": -9.5347089767456055, + "rotationX": -2.1324558474589139E-07, + "rotationY": 2.0361580027383752E-05, + "rotationZ": -4.7822368287597783E-08, + "rotationW": 1, + "time": 1686384931 + }, + { + "flags": 0, + "furnitureObjectId": "00000000-00000000-00000002-00000005", + "instanceId": "4874595597", + "itemId": 12, + "positionX": -3.5068926811218262, + "positionY": 3.4883472919464111, + "positionZ": -9.5313901901245117, + "rotationX": -0.00091801158851012588, + "rotationY": 0.006055513396859169, + "rotationZ": 0.000585820700507611, + "rotationW": 0.99998104572296143, + "time": 1686384961, + "photo": "/Furniture/Modern2/lampOutputcube.dds" + }, + { + "flags": 0, + "furnitureObjectId": "00000000-00000000-00000002-00000005", + "instanceId": "4874595598", + "itemId": 13, + "positionX": 3.6171293258666992, + "positionY": 3.4891724586486816, + "positionZ": -9.53490161895752, + "rotationX": 0.00042979296995326877, + "rotationY": -0.0092521701008081436, + "rotationZ": -0.00027207753737457097, + "rotationW": 0.99995702505111694, + "time": 1686385008, + "photo": "/Furniture/Modern2/lampOutputcube.dds" + } + ] + } + } +] \ No newline at end of file