using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Text.Json; using OpenSaveCloudClient.Models.Remote; using OpenSaveCloudClient.Models; using System.Net.Http.Headers; using System.IO.Compression; namespace OpenSaveCloudClient.Core { /// /// This class is a connector to the remote Open Save Cloud server, it contains all the function that are mapped to the server endpoint /// This is a singleton, to get the instance, call GetInstance() /// public class ServerConnector { private static ServerConnector? instance; private string? token; private string? host; private int port; private bool bind; private bool connected; private ServerInformation? serverInformation; private User? connectedUser; private LogManager logManager; private TaskManager taskManager; private Configuration configuration; private SaveManager saveManager; public string? Host { get { return host; } } public int Port { get { return port; } } public bool Bind { get { return bind; } } public bool Connected { get { return connected; } } public User? ConnectedUser { get { return connectedUser; } } public ServerInformation? ServerInformation { get { return serverInformation; } } private ServerConnector() { configuration = Configuration.GetInstance(); logManager = LogManager.GetInstance(); taskManager = TaskManager.GetInstance(); saveManager = SaveManager.GetInstance(); } public static ServerConnector GetInstance() { if (instance == null) { instance = new ServerConnector(); } return instance; } /// /// method BindNewServer set the hostname (or ip) and the port of the server and try to connect /// /// hostname or IP of the server /// port of the server public void BindNewServer(string host, int port) { Logout(); if (!host.StartsWith("http://") && !host.StartsWith("https://")) { host = "http://" + host; } logManager.AddInformation(String.Format("Binding server {0}:{1}", host, port)); this.host = host; this.port = port; GetServerInformation(); } /// /// method Login connect a user and save the token to token /// /// Username of the user /// Password of the user public void Login(string username, string password) { logManager.AddInformation("Loging in to the server"); string uuidTask = taskManager.StartTask("Login to the server", true, 1); try { HttpClient client = new HttpClient(); string json = JsonSerializer.Serialize(new Credential { Username = username, Password = password }); HttpContent content = new StringContent(json, Encoding.UTF8, "application/json"); HttpResponseMessage response = client.PostAsync(string.Format("{0}:{1}/api/v1/login", host, port), content).Result; if (response.IsSuccessStatusCode) { string responseText = response.Content.ReadAsStringAsync().Result; AccessToken? accessToken = JsonSerializer.Deserialize(responseText); if (accessToken != null) { token = accessToken.Token; connected = true; connectedUser = GetConnectedUserInformation(); SaveToConfig(); taskManager.UpdateTaskStatus(uuidTask, AsyncTaskStatus.Ended); } else { taskManager.UpdateTaskStatus(uuidTask, AsyncTaskStatus.Failed); } } else { taskManager.UpdateTaskStatus(uuidTask, AsyncTaskStatus.Failed); } } catch (Exception ex) { logManager.AddError(ex); taskManager.UpdateTaskStatus(uuidTask, AsyncTaskStatus.Failed); } } /// /// method Reconnect try to reconnect with the token, hostname and port saved in the configuration file /// public void Reconnect() { string? uuidTask = null; try { if (ReloadFromConfiguration()) { uuidTask = taskManager.StartTask("Login to the server", true, 1); HttpClient client = new HttpClient(); string json = JsonSerializer.Serialize(new AccessToken { Token = token }); HttpContent content = new StringContent(json, Encoding.UTF8, "application/json"); HttpResponseMessage response = client.PostAsync(string.Format("{0}:{1}/api/v1/check/token", host, port), content).Result; if (response.IsSuccessStatusCode) { string responseText = response.Content.ReadAsStringAsync().Result; TokenValidation? accessToken = JsonSerializer.Deserialize(responseText); if (accessToken != null && accessToken.Valid) { connected = true; connectedUser = GetConnectedUserInformation(); SaveToConfig(); taskManager.UpdateTaskStatus(uuidTask, AsyncTaskStatus.Ended); } else { Logout(); taskManager.UpdateTaskStatus(uuidTask, AsyncTaskStatus.Failed); } } else { Logout(); taskManager.UpdateTaskStatus(uuidTask, AsyncTaskStatus.Failed); } } } catch (Exception ex) { logManager.AddError(ex); if (uuidTask != null) { taskManager.UpdateTaskStatus(uuidTask, AsyncTaskStatus.Failed); } } } /// /// method Logout disconnect the current user and remove all the server's information of the config file /// public void Logout() { serverInformation = null; bind = false; connected = false; token = ""; configuration.SetValue("authentication.host", null); configuration.SetValue("authentication.port", null); configuration.SetValue("authentication.token", null); configuration.Flush(); } /// /// method CreateGame create a new game entry in the server database /// /// The name of the game /// The game that was created by the server public Game? CreateGame(string name) { logManager.AddInformation("Creating game to server database"); string uuidTask = taskManager.StartTask("Creating game to server database", true, 1); try { HttpClient client = new HttpClient(); string json = JsonSerializer.Serialize(new NewGameInfo { Name = name }); HttpContent content = new StringContent(json, Encoding.UTF8, "application/json"); client.DefaultRequestHeaders.Add("Authorization", "bearer " + token); HttpResponseMessage response = client.PostAsync(string.Format("{0}:{1}/api/v1/game/create", host, port), content).Result; if (response.IsSuccessStatusCode) { logManager.AddInformation("Game created!"); string responseText = response.Content.ReadAsStringAsync().Result; taskManager.UpdateTaskStatus(uuidTask, AsyncTaskStatus.Ended); return JsonSerializer.Deserialize(responseText); } else { logManager.AddError(String.Format("Received HTTP Status {0} from the server", response.StatusCode.ToString())); } taskManager.UpdateTaskStatus(uuidTask, AsyncTaskStatus.Failed); } catch (Exception ex) { logManager.AddError(ex); taskManager.UpdateTaskStatus(uuidTask, AsyncTaskStatus.Failed); } return null; } /// /// method Synchronize is the method that upload and download the files from the server /// If there is conflict, a warning is generated and the files are keep untouched /// public async void Synchronize() { logManager.AddInformation("Starting synchronization"); List games = saveManager.Saves; string uuidTask = taskManager.StartTask("Synchronizing games", true, games.Count); List toUpload = new(); List toDownload = new(); foreach (GameSave localCopy in games) { try { // Get the current information that are stored in the server Game? remoteCopy = GetGameInfoByID(localCopy.Id); if (remoteCopy == null) { logManager.AddWarning(String.Format("'{0}' is not found on this server, force upload it from the game detail screen", localCopy.Name)); continue; } // Check if available on the server if (!remoteCopy.Available) { logManager.AddInformation(String.Format("'{0}' does not exist in the server", localCopy.Name)); localCopy.DetectChanges(); toUpload.Add(localCopy); } else if (localCopy.DetectChanges() || !localCopy.Synced) { // Create an archive of the folder localCopy.Archive(); // Upload only if the revision is the same if (remoteCopy.Revision != localCopy.Revision) { logManager.AddWarning(String.Format("There revision of the local copy is not equal with the copy on the server ({0})", localCopy.Name)); logManager.AddInformation("To resolve this conflict, force download or force upload from the game detail screen"); continue; } toUpload.Add(localCopy); } else { if (remoteCopy.Revision > localCopy.Revision) { toDownload.Add(localCopy); } else if (remoteCopy.Revision < localCopy.Revision) { logManager.AddWarning(String.Format("There revision of the local copy is not equal with the copy on the server ({0})", localCopy.Name)); logManager.AddInformation("To resolve this conflict, force download or force upload from the game detail screen"); } } } catch (Exception ex) { logManager.AddError(ex); } taskManager.UpdateTaskProgress(uuidTask, 1); } // Upload files UploadGames(toUpload); // Download new version of files await DownloadGamesAsync(toDownload); taskManager.UpdateTaskStatus(uuidTask, AsyncTaskStatus.Ended); } /// /// method UploadGames upload the game saves to the server /// /// A list of GameSaves public void UploadGames(List toUpload) { string cachePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "osc", "cache"); foreach (GameSave game in toUpload) { GameUploadToken? gut = LockGameToUpload(game.Id); if (gut != null) { string archivePath = Path.Combine(cachePath, game.Uuid + ".bin"); if (UploadSave(gut.UploadToken, archivePath, game.CurrentHash)) { game.UpdateHash(); UpdateCache(game); } } } saveManager.Save(); } /// /// method DownloadGamesAsync download the game saves from the server /// This method is async because of DownloadSaveAsync that are async too /// /// A list of GameSaves /// A task public async Task DownloadGamesAsync(List toDownload) { string cachePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "osc", "cache"); if (!Directory.Exists(cachePath)) { Directory.CreateDirectory(cachePath); } string archivePath; GameUploadToken? gut; foreach (GameSave game in toDownload) { gut = LockGameToUpload(game.Id); if (gut != null) { archivePath = Path.Combine(cachePath, game.Uuid + ".bin"); if (await DownloadSaveAsync(gut.UploadToken, archivePath, game.FolderPath)) { game.DetectChanges(); game.UpdateHash(); UpdateCache(game); } } } saveManager.Save(); } /// /// method GetGameInfoByID get the game save information from the server /// /// A game id /// A remote object of a game save public Game? GetGameInfoByID(long gameId) { logManager.AddInformation("Getting game information from the server database"); string uuidTask = taskManager.StartTask("Getting game information", true, 1); try { using (HttpClient client = new HttpClient()) { client.DefaultRequestHeaders.Add("Authorization", "bearer " + token); HttpResponseMessage response = client.GetAsync(string.Format("{0}:{1}/api/v1/game/info/{2}", host, port, gameId)).Result; if (response.IsSuccessStatusCode) { string responseText = response.Content.ReadAsStringAsync().Result; taskManager.UpdateTaskStatus(uuidTask, AsyncTaskStatus.Ended); return JsonSerializer.Deserialize(responseText); } else { logManager.AddError(String.Format("Received HTTP Status {0} from the server", response.StatusCode.ToString())); } taskManager.UpdateTaskStatus(uuidTask, AsyncTaskStatus.Failed); } } catch (Exception ex) { logManager.AddError(ex); taskManager.UpdateTaskStatus(uuidTask, AsyncTaskStatus.Failed); } return null; } /// /// method GetGamesInfo get all the save registered on the server of the current user /// /// A game id /// A list of remote object of a game save public List? GetGamesInfo() { logManager.AddInformation("Getting game information from the server database"); string uuidTask = taskManager.StartTask("Getting game information", true, 1); try { using (HttpClient client = new HttpClient()) { client.DefaultRequestHeaders.Add("Authorization", "bearer " + token); HttpResponseMessage response = client.GetAsync(string.Format("{0}:{1}/api/v1/game/all", host, port)).Result; if (response.IsSuccessStatusCode) { string responseText = response.Content.ReadAsStringAsync().Result; taskManager.UpdateTaskStatus(uuidTask, AsyncTaskStatus.Ended); return JsonSerializer.Deserialize>(responseText); } else { logManager.AddError(String.Format("Received HTTP Status {0} from the server", response.StatusCode.ToString())); } taskManager.UpdateTaskStatus(uuidTask, AsyncTaskStatus.Failed); } } catch (Exception ex) { logManager.AddError(ex); taskManager.UpdateTaskStatus(uuidTask, AsyncTaskStatus.Failed); } return null; } /// /// method UploadSave upload a file to the server /// /// The Lock token that a provided by the server /// The path of the file /// The new hash of the folder public bool UploadSave(string uploadToken, string filePath, string newHash) { logManager.AddInformation("Uploading save"); string uuidTask = taskManager.StartTask("Uploading", true, 1); FileStream stream = File.OpenRead(filePath); try { MultipartFormDataContent multipartFormContent = new(); var fileStreamContent = new StreamContent(stream); fileStreamContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); multipartFormContent.Add(fileStreamContent, name: "file", fileName: "file.bin"); using (HttpClient client = new HttpClient()) { client.DefaultRequestHeaders.Add("Authorization", "bearer " + token); client.DefaultRequestHeaders.Add("X-Upload-Key", uploadToken); client.DefaultRequestHeaders.Add("X-Game-Save-Hash", newHash); HttpResponseMessage response = client.PostAsync(string.Format("{0}:{1}/api/v1/game/upload", host, port), multipartFormContent).Result; if (response.IsSuccessStatusCode) { taskManager.UpdateTaskStatus(uuidTask, AsyncTaskStatus.Ended); return true; } else { logManager.AddError(String.Format("Received HTTP Status {0} from the server", response.StatusCode.ToString())); } taskManager.UpdateTaskStatus(uuidTask, AsyncTaskStatus.Failed); } } catch (Exception ex) { logManager.AddError(ex); taskManager.UpdateTaskStatus(uuidTask, AsyncTaskStatus.Failed); } finally { stream.Close(); } return false; } /// /// /// /// The Lock token that a provided by the server /// The path where the downloaded archive will be stored /// The path is stored the save /// If the save is successfully downloaded and unpacked public async Task DownloadSaveAsync(string uploadToken, string filePath, string unzipPath) { logManager.AddInformation("Downloading save"); string uuidTask = taskManager.StartTask("Downloading", true, 1); try { using (HttpClient client = new HttpClient()) { client.DefaultRequestHeaders.Add("Authorization", "bearer " + token); client.DefaultRequestHeaders.Add("X-Upload-Key", uploadToken); HttpResponseMessage response = client.GetAsync(string.Format("{0}:{1}/api/v1/game/download", host, port)).Result; if (response.IsSuccessStatusCode) { using (var fs = new FileStream(filePath, FileMode.Create)) { await response.Content.CopyToAsync(fs); } if (Directory.Exists(unzipPath)) { Directory.Delete(unzipPath, true); } ZipFile.ExtractToDirectory(filePath, unzipPath); taskManager.UpdateTaskStatus(uuidTask, AsyncTaskStatus.Ended); return true; } else { logManager.AddError(String.Format("Received HTTP Status {0} from the server", response.StatusCode.ToString())); taskManager.UpdateTaskStatus(uuidTask, AsyncTaskStatus.Failed); } } } catch (Exception ex) { logManager.AddError(ex); taskManager.UpdateTaskStatus(uuidTask, AsyncTaskStatus.Failed); } return false; } public User? GetConnectedUserInformation() { logManager.AddInformation("Getting user information from the server database"); string uuidTask = taskManager.StartTask("Getting user information", true, 1); try { using (HttpClient client = new HttpClient()) { client.DefaultRequestHeaders.Add("Authorization", "bearer " + token); HttpResponseMessage response = client.GetAsync(string.Format("{0}:{1}/api/v1/user/information", host, port)).Result; if (response.IsSuccessStatusCode) { string responseText = response.Content.ReadAsStringAsync().Result; taskManager.UpdateTaskStatus(uuidTask, AsyncTaskStatus.Ended); return JsonSerializer.Deserialize(responseText); } else { logManager.AddError(String.Format("Received HTTP Status {0} from the server", response.StatusCode.ToString())); } taskManager.UpdateTaskStatus(uuidTask, AsyncTaskStatus.Failed); } } catch (Exception ex) { logManager.AddError(ex); taskManager.UpdateTaskStatus(uuidTask, AsyncTaskStatus.Failed); } return null; } public List? GetUsers() { logManager.AddInformation("Getting all users from the server database"); string uuidTask = taskManager.StartTask("Getting users list", true, 1); try { using (HttpClient client = new HttpClient()) { client.DefaultRequestHeaders.Add("Authorization", "bearer " + token); HttpResponseMessage response = client.GetAsync(string.Format("{0}:{1}/api/v1/system/users", host, port)).Result; if (response.IsSuccessStatusCode) { string responseText = response.Content.ReadAsStringAsync().Result; taskManager.UpdateTaskStatus(uuidTask, AsyncTaskStatus.Ended); return JsonSerializer.Deserialize>(responseText); } else { logManager.AddError(String.Format("Received HTTP Status {0} from the server", response.StatusCode.ToString())); } taskManager.UpdateTaskStatus(uuidTask, AsyncTaskStatus.Failed); } } catch (Exception ex) { logManager.AddError(ex); taskManager.UpdateTaskStatus(uuidTask, AsyncTaskStatus.Failed); } return null; } public bool ChangePassword(NewPassword password) { logManager.AddInformation("Changing password"); string uuidTask = taskManager.StartTask("Changing password", true, 1); try { HttpClient client = new HttpClient(); string json = JsonSerializer.Serialize(password); HttpContent content = new StringContent(json, Encoding.UTF8, "application/json"); client.DefaultRequestHeaders.Add("Authorization", "bearer " + token); HttpResponseMessage response = client.PostAsync(string.Format("{0}:{1}/api/v1/user/passwd", host, port), content).Result; if (response.IsSuccessStatusCode) { logManager.AddInformation("Password changed"); string responseText = response.Content.ReadAsStringAsync().Result; taskManager.UpdateTaskStatus(uuidTask, AsyncTaskStatus.Ended); return true; } else { logManager.AddError(String.Format("Received HTTP Status {0} from the server", response.StatusCode.ToString())); } taskManager.UpdateTaskStatus(uuidTask, AsyncTaskStatus.Failed); } catch (Exception ex) { logManager.AddError(ex); taskManager.UpdateTaskStatus(uuidTask, AsyncTaskStatus.Failed); } return false; } /// /// method UpdateCache update the GameSave object with the server data /// /// A GameSave object private void UpdateCache(GameSave gameSave) { string uuidTask = taskManager.StartTask("Updating cache", true, 1); Game? game = GetGameInfoByID(gameSave.Id); if (game != null) { gameSave.Revision = game.Revision; gameSave.Synced = true; taskManager.UpdateTaskStatus(uuidTask, AsyncTaskStatus.Ended); } else { logManager.AddError("Failed to get game information"); taskManager.UpdateTaskStatus(uuidTask, AsyncTaskStatus.Failed); } } /// /// method LockGameToUpload lock a game save on the server /// This method is useful to avoid competing uploads/downloads /// /// A game id /// A token to give to the upload/download method private GameUploadToken? LockGameToUpload(long gameId) { logManager.AddInformation("Locking game in the server"); string uuidTask = taskManager.StartTask("Locking game", true, 1); try { using (HttpClient client = new HttpClient()) { string json = JsonSerializer.Serialize(new UploadGameInfo { GameId = gameId }); HttpContent content = new StringContent(json, Encoding.UTF8, "application/json"); client.DefaultRequestHeaders.Add("Authorization", "bearer " + token); HttpResponseMessage response = client.PostAsync(string.Format("{0}:{1}/api/v1/game/upload/init", host, port), content).Result; if (response.IsSuccessStatusCode) { logManager.AddInformation("Game locked"); string responseText = response.Content.ReadAsStringAsync().Result; taskManager.UpdateTaskStatus(uuidTask, AsyncTaskStatus.Ended); return JsonSerializer.Deserialize(responseText); } else { logManager.AddError(String.Format("Received HTTP Status {0} from the server", response.StatusCode.ToString())); } taskManager.UpdateTaskStatus(uuidTask, AsyncTaskStatus.Failed); } } catch (Exception ex) { logManager.AddError(ex); taskManager.UpdateTaskStatus(uuidTask, AsyncTaskStatus.Failed); } return null; } /// /// method ReloadFromConfiguration load the server information (host, port and token) from the configuration file /// /// The configuration is valid private bool ReloadFromConfiguration() { string newHost = configuration.GetString("authentication.host", ""); int newPort = configuration.GetInt("authentication.port", 443); if (string.IsNullOrWhiteSpace(newHost)) { return false; } try { string oldToken = configuration.GetString("authentication.token", ""); BindNewServer(newHost, newPort); if (!bind) { return false; } if (string.IsNullOrWhiteSpace(oldToken)) { return false; } token = oldToken; } catch (Exception ex) { logManager.AddError(ex); return false; } return true; } /// /// method GetServerInformation get information about the connected server /// private void GetServerInformation() { logManager.AddInformation("Getting server information"); string uuidTask = taskManager.StartTask("Getting server information", true, 1); try { HttpClient client = new(); HttpResponseMessage response = client.GetAsync(string.Format("{0}:{1}/api/v1/system/information", host, port)).Result; if (response.IsSuccessStatusCode) { string responseText = response.Content.ReadAsStringAsync().Result; serverInformation = JsonSerializer.Deserialize(responseText); if (serverInformation != null) { logManager.AddInformation("Server is connected"); bind = true; taskManager.UpdateTaskStatus(uuidTask, AsyncTaskStatus.Ended); return; } } } catch (Exception ex) { logManager.AddError(ex); } taskManager.UpdateTaskStatus(uuidTask, AsyncTaskStatus.Failed); } /// /// method SaveToConfig save the server connection information to the configuration file /// private void SaveToConfig() { configuration.SetValue("authentication.host", host); configuration.SetValue("authentication.port", port); configuration.SetValue("authentication.token", token); configuration.Flush(); } } }