diff --git a/Dotnet/AppApi/Cef/AppApiCef.cs b/Dotnet/AppApi/Cef/AppApiCef.cs index 1c38f594..a56f16c8 100644 --- a/Dotnet/AppApi/Cef/AppApiCef.cs +++ b/Dotnet/AppApi/Cef/AppApiCef.cs @@ -36,14 +36,6 @@ namespace VRCX MainForm.Instance.Browser.ShowDevTools(); } - /// - /// Deletes all cookies from the global cef cookie manager. - /// - public override void DeleteAllCookies() - { - Cef.GetGlobalCookieManager().DeleteCookies(); - } - public override void SetVR(bool active, bool hmdOverlay, bool wristOverlay, bool menuButton, int overlayHand) { Program.VRCXVRInstance.SetActive(active, hmdOverlay, wristOverlay, menuButton, overlayHand); diff --git a/Dotnet/AppApi/Common/AppApiCommonBase.cs b/Dotnet/AppApi/Common/AppApiCommonBase.cs index 7553a27a..e7a06641 100644 --- a/Dotnet/AppApi/Common/AppApiCommonBase.cs +++ b/Dotnet/AppApi/Common/AppApiCommonBase.cs @@ -8,7 +8,6 @@ namespace VRCX { // AppApi public abstract void ShowDevTools(); - public abstract void DeleteAllCookies(); public abstract void SetVR(bool active, bool hmdOverlay, bool wristOverlay, bool menuButton, int overlayHand); public abstract void RefreshVR(); public abstract void RestartVR(); diff --git a/Dotnet/AppApi/Common/ImageSaving.cs b/Dotnet/AppApi/Common/ImageSaving.cs index 1443205a..962f2dd8 100644 --- a/Dotnet/AppApi/Common/ImageSaving.cs +++ b/Dotnet/AppApi/Common/ImageSaving.cs @@ -226,9 +226,17 @@ namespace VRCX if (File.Exists(filePath)) return null; - var success = await ImageCache.SaveImageToFile(url, filePath); - - return success ? filePath : null; + try + { + await ImageCache.SaveImageToFile(url, filePath); + } + catch (Exception ex) + { + logger.Error(ex, "Failed to save print to file"); + return null; + } + + return filePath; } public async Task SaveStickerToFile(string url, string ugcFolderPath, string monthFolder, string fileName) @@ -239,9 +247,17 @@ namespace VRCX if (File.Exists(filePath)) return null; - var success = await ImageCache.SaveImageToFile(url, filePath); - - return success ? filePath : null; + try + { + await ImageCache.SaveImageToFile(url, filePath); + } + catch (Exception ex) + { + logger.Error(ex, "Failed to save print to file"); + return null; + } + + return filePath; } } } \ No newline at end of file diff --git a/Dotnet/AppApi/Electron/AppApiElectron.cs b/Dotnet/AppApi/Electron/AppApiElectron.cs index b6123cf6..aaeed7bf 100644 --- a/Dotnet/AppApi/Electron/AppApiElectron.cs +++ b/Dotnet/AppApi/Electron/AppApiElectron.cs @@ -10,10 +10,6 @@ namespace VRCX { private static readonly Logger logger = LogManager.GetCurrentClassLogger(); - public override void DeleteAllCookies() - { - } - public override void ShowDevTools() { } diff --git a/Dotnet/Cef/CefService.cs b/Dotnet/Cef/CefService.cs index 243a77bd..a28ac5d9 100644 --- a/Dotnet/Cef/CefService.cs +++ b/Dotnet/Cef/CefService.cs @@ -81,11 +81,11 @@ namespace VRCX var vueDevtoolsCrxPath = Path.Join(Program.BaseDirectory, @"..\..\build-tools\Vue-js-devtools.crx"); if (File.Exists(vueDevtoolsCrxPath)) { - var VueDevtoolsPath = Path.Join(extensionsPath, "Vue-js-devtools"); - if (!Directory.Exists(VueDevtoolsPath)) + var vueDevtoolsPath = Path.Join(extensionsPath, "Vue-js-devtools"); + if (!Directory.Exists(vueDevtoolsPath)) { - Directory.CreateDirectory(VueDevtoolsPath); - ZipFile.ExtractToDirectory(vueDevtoolsCrxPath, VueDevtoolsPath); + Directory.CreateDirectory(vueDevtoolsPath); + ZipFile.ExtractToDirectory(vueDevtoolsCrxPath, vueDevtoolsPath); } } diff --git a/Dotnet/ImageCache.cs b/Dotnet/ImageCache.cs index 8a0b695b..5717daf7 100644 --- a/Dotnet/ImageCache.cs +++ b/Dotnet/ImageCache.cs @@ -5,11 +5,13 @@ using System.Net; using System.Linq; using System.Net.Http; using System.Threading.Tasks; +using NLog; namespace VRCX; internal static class ImageCache { + private static readonly Logger logger = LogManager.GetCurrentClassLogger(); private static readonly string cacheLocation; private static readonly HttpClient httpClient; private static readonly List ImageHosts = @@ -47,6 +49,31 @@ internal static class ImageCache } } + private static async Task FetchImage(string url) + { + var uri = new Uri(url); + if (!ImageHosts.Contains(uri.Host)) + throw new ArgumentException("Invalid image host", url); + + var cookieString = string.Empty; + if (WebApi.Instance != null && + WebApi.Instance._cookieContainer != null && + uri.Host == "api.vrchat.cloud") + { + CookieCollection cookies = WebApi.Instance._cookieContainer.GetCookies(new Uri("https://api.vrchat.cloud")); + foreach (Cookie cookie in cookies) + cookieString += $"{cookie.Name}={cookie.Value};"; + } + + var request = new HttpRequestMessage(HttpMethod.Get, url); + if (!string.IsNullOrEmpty(cookieString)) + request.Headers.Add("Cookie", cookieString); + + using var response = await httpClient.SendAsync(request); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadAsStreamAsync(); + } + public static async Task GetImage(string url, string fileId, string version) { var directoryLocation = Path.Join(cacheLocation, fileId); @@ -62,29 +89,17 @@ internal static class ImageCache Directory.Delete(directoryLocation, true); Directory.CreateDirectory(directoryLocation); - var uri = new Uri(url); - if (!ImageHosts.Contains(uri.Host)) - throw new ArgumentException("Invalid image host", url); - - var cookieString = string.Empty; - if (WebApi.Instance != null && WebApi.Instance._cookieContainer != null) + try { - CookieCollection cookies = WebApi.Instance._cookieContainer.GetCookies(new Uri("https://api.vrchat.cloud")); - foreach (Cookie cookie in cookies) - cookieString += $"{cookie.Name}={cookie.Value};"; + var stream = await FetchImage(url); + await using var fileStream = + new FileStream(fileLocation, FileMode.Create, FileAccess.Write, FileShare.None); + await stream.CopyToAsync(fileStream); } - - var request = new HttpRequestMessage(HttpMethod.Get, url); - if (!string.IsNullOrEmpty(cookieString)) - request.Headers.Add("Cookie", cookieString); - - using (var response = await httpClient.SendAsync(request)) + catch (Exception ex) { - response.EnsureSuccessStatusCode(); - await using (var fileStream = new FileStream(fileLocation, FileMode.Create, FileAccess.Write, FileShare.None)) - { - await response.Content.CopyToAsync(fileStream); - } + logger.Error(ex, "Failed to fetch image"); + return string.Empty; } var cacheSize = Directory.GetDirectories(cacheLocation).Length; @@ -94,6 +109,13 @@ internal static class ImageCache return fileLocation; } + public static async Task SaveImageToFile(string url, string path) + { + var stream = await FetchImage(url); + await using var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None); + await stream.CopyToAsync(fileStream); + } + private static void CleanImageCache() { var dirInfo = new DirectoryInfo(cacheLocation); @@ -103,31 +125,4 @@ internal static class ImageCache folder.Delete(true); } } - - public static async Task SaveImageToFile(string url, string path) - { - var uri = new Uri(url); - if (!ImageHosts.Contains(uri.Host)) - throw new ArgumentException("Invalid image host", url); - - var cookieString = string.Empty; - if (WebApi.Instance != null && WebApi.Instance._cookieContainer != null) - { - var cookies = WebApi.Instance._cookieContainer.GetCookies(new Uri("https://api.vrchat.cloud")); - foreach (Cookie cookie in cookies) - cookieString += $"{cookie.Name}={cookie.Value};"; - } - - var request = new HttpRequestMessage(HttpMethod.Get, url); - if (!string.IsNullOrEmpty(cookieString)) - request.Headers.Add("Cookie", cookieString); - - using var response = await httpClient.SendAsync(request); - if (!response.IsSuccessStatusCode) - return false; - - await using var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None); - await response.Content.CopyToAsync(fileStream); - return true; - } } \ No newline at end of file diff --git a/Dotnet/WebApi.cs b/Dotnet/WebApi.cs index c31e4cec..a9180cc8 100644 --- a/Dotnet/WebApi.cs +++ b/Dotnet/WebApi.cs @@ -1,7 +1,10 @@ using System; +using System.Collections; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Net; +using System.Reflection; using System.Text; using System.Threading.Tasks; using Newtonsoft.Json; @@ -37,7 +40,7 @@ namespace VRCX ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12; } - public WebApi() + private WebApi() { #if LINUX if (Instance == null) @@ -89,7 +92,7 @@ namespace VRCX catch (UriFormatException) { VRCXStorage.Instance.Set("VRCX_ProxyServer", string.Empty); - var message = "The proxy server URI you used is invalid.\nVRCX will close, please correct the proxy URI."; + const string message = "The proxy server URI you used is invalid.\nVRCX will close, please correct the proxy URI."; #if !LINUX System.Windows.Forms.MessageBox.Show(message, "Invalid Proxy URI", MessageBoxButtons.OK, MessageBoxIcon.Error); #endif @@ -106,6 +109,9 @@ namespace VRCX public void ClearCookies() { +#if !LINUX + Cef.GetGlobalCookieManager().DeleteCookies(); +#endif _cookieContainer = new CookieContainer(); SaveCookies(); } @@ -125,38 +131,70 @@ namespace VRCX using var stream = new MemoryStream(Convert.FromBase64String((string)item[0])); _cookieContainer = new CookieContainer(); _cookieContainer.Add(System.Text.Json.JsonSerializer.Deserialize(stream)); - // _cookieContainer = (CookieContainer)new BinaryFormatter().Deserialize(stream); // from .NET framework } catch (Exception e) { Logger.Error($"Failed to load cookies: {e.Message}"); } } + + private List GetAllCookies() + { + var cookieTable = (Hashtable)_cookieContainer.GetType().InvokeMember("m_domainTable", + BindingFlags.NonPublic | + BindingFlags.GetField | + BindingFlags.Instance, + null, + _cookieContainer, + new object[] { }); + + var uniqueCookies = new Dictionary(); + foreach (var item in cookieTable.Keys) + { + var domain = (string)item; + if (string.IsNullOrEmpty(domain)) + continue; + + if (domain.StartsWith('.')) + domain = domain[1..]; + + var address = $"http://{domain}/"; + if (!Uri.TryCreate(address, UriKind.Absolute, out var uri)) + continue; + + foreach (Cookie cookie in _cookieContainer.GetCookies(uri)) + { + var key = $"{domain}.{cookie.Name}"; + if (!uniqueCookies.TryGetValue(key, out var value) || + cookie.TimeStamp > value.TimeStamp) + { + cookie.Expires = DateTime.MaxValue; + uniqueCookies[key] = cookie; + } + } + } + + return uniqueCookies.Values.ToList(); + } public void SaveCookies() { - if (_cookieDirty == false) - { + if (!_cookieDirty) return; - } - foreach (Cookie cookie in _cookieContainer.GetAllCookies()) - { - cookie.Expires = DateTime.MaxValue; - } + try { - using (var memoryStream = new MemoryStream()) - { - System.Text.Json.JsonSerializer.Serialize(memoryStream, _cookieContainer.GetAllCookies()); - //new BinaryFormatter().Serialize(memoryStream, _cookieContainer); - SQLiteLegacy.Instance.ExecuteNonQuery( - "INSERT OR REPLACE INTO `cookies` (`key`, `value`) VALUES (@key, @value)", - new Dictionary() { - {"@key", "default"}, - {"@value", Convert.ToBase64String(memoryStream.ToArray())} - } - ); - } + var cookies = GetAllCookies(); + using var memoryStream = new MemoryStream(); + System.Text.Json.JsonSerializer.Serialize(memoryStream, cookies); + SQLiteLegacy.Instance.ExecuteNonQuery( + "INSERT OR REPLACE INTO `cookies` (`key`, `value`) VALUES (@key, @value)", + new Dictionary() { + {"@key", "default"}, + {"@value", Convert.ToBase64String(memoryStream.ToArray())} + } + ); + _cookieDirty = false; } catch (Exception e) @@ -170,7 +208,7 @@ namespace VRCX _cookieDirty = true; // force cookies to be saved for lastUserLoggedIn using var memoryStream = new MemoryStream(); - System.Text.Json.JsonSerializer.Serialize(memoryStream, _cookieContainer.GetAllCookies()); + System.Text.Json.JsonSerializer.Serialize(memoryStream, GetAllCookies()); return Convert.ToBase64String(memoryStream.ToArray()); } @@ -192,44 +230,44 @@ namespace VRCX request.AutomaticDecompression = DecompressionMethods.All; request.Method = "POST"; - string boundary = "---------------------------" + DateTime.Now.Ticks.ToString("x"); + var boundary = "---------------------------" + DateTime.Now.Ticks.ToString("x"); request.ContentType = "multipart/form-data; boundary=" + boundary; - Stream requestStream = request.GetRequestStream(); - if (options.TryGetValue("postData", out object postDataObject) == true) + var requestStream = request.GetRequestStream(); + if (options.TryGetValue("postData", out var postDataObject)) { - Dictionary postData = new Dictionary(); + var postData = new Dictionary(); postData.Add("data", (string)postDataObject); - string FormDataTemplate = "--{0}\r\nContent-Disposition: form-data; name=\"{1}\"\r\n\r\n{2}\r\n"; - foreach (string key in postData.Keys) + const string formDataTemplate = "--{0}\r\nContent-Disposition: form-data; name=\"{1}\"\r\n\r\n{2}\r\n"; + foreach (var key in postData.Keys) { - string item = string.Format(FormDataTemplate, boundary, key, postData[key]); - byte[] itemBytes = Encoding.UTF8.GetBytes(item); - await requestStream.WriteAsync(itemBytes, 0, itemBytes.Length); + var item = string.Format(formDataTemplate, boundary, key, postData[key]); + var itemBytes = Encoding.UTF8.GetBytes(item); + await requestStream.WriteAsync(itemBytes); } } var imageData = options["imageData"] as string; - byte[] fileToUpload = Program.AppApiInstance.ResizeImageToFitLimits(Convert.FromBase64String(imageData), false); - string fileFormKey = "image"; - string fileName = "image.png"; - string fileMimeType = "image/png"; - string HeaderTemplate = "--{0}\r\nContent-Disposition: form-data; name=\"{1}\"; filename=\"{2}\"\r\nContent-Type: {3}\r\n\r\n"; - string header = string.Format(HeaderTemplate, boundary, fileFormKey, fileName, fileMimeType); - byte[] headerbytes = Encoding.UTF8.GetBytes(header); - await requestStream.WriteAsync(headerbytes, 0, headerbytes.Length); - using (MemoryStream fileStream = new MemoryStream(fileToUpload)) + var fileToUpload = Program.AppApiInstance.ResizeImageToFitLimits(Convert.FromBase64String(imageData), false); + const string fileFormKey = "image"; + const string fileName = "image.png"; + const string fileMimeType = "image/png"; + const string headerTemplate = "--{0}\r\nContent-Disposition: form-data; name=\"{1}\"; filename=\"{2}\"\r\nContent-Type: {3}\r\n\r\n"; + var header = string.Format(headerTemplate, boundary, fileFormKey, fileName, fileMimeType); + var headerBytes = Encoding.UTF8.GetBytes(header); + await requestStream.WriteAsync(headerBytes); + using (var fileStream = new MemoryStream(fileToUpload)) { - byte[] buffer = new byte[1024]; - int bytesRead = 0; + var buffer = new byte[1024]; + var bytesRead = 0; while ((bytesRead = fileStream.Read(buffer, 0, buffer.Length)) != 0) { - await requestStream.WriteAsync(buffer, 0, bytesRead); + await requestStream.WriteAsync(buffer.AsMemory(0, bytesRead)); } fileStream.Close(); } - byte[] newlineBytes = Encoding.UTF8.GetBytes("\r\n"); - await requestStream.WriteAsync(newlineBytes, 0, newlineBytes.Length); - byte[] endBytes = Encoding.UTF8.GetBytes("--" + boundary + "--"); - await requestStream.WriteAsync(endBytes, 0, endBytes.Length); + var newlineBytes = Encoding.UTF8.GetBytes("\r\n"); + await requestStream.WriteAsync(newlineBytes); + var endBytes = Encoding.UTF8.GetBytes("--" + boundary + "--"); + await requestStream.WriteAsync(endBytes); requestStream.Close(); } @@ -244,11 +282,9 @@ namespace VRCX var fileData = options["fileData"] as string; var sentData = Convert.FromBase64CharArray(fileData.ToCharArray(), 0, fileData.Length); request.ContentLength = sentData.Length; - using (var sendStream = request.GetRequestStream()) - { - await sendStream.WriteAsync(sentData, 0, sentData.Length); - sendStream.Close(); - } + await using var sendStream = request.GetRequestStream(); + await sendStream.WriteAsync(sentData); + sendStream.Close(); } private static async Task ImageUpload(HttpWebRequest request, IDictionary options) @@ -258,48 +294,48 @@ namespace VRCX request.AutomaticDecompression = DecompressionMethods.All; request.Method = "POST"; - string boundary = "---------------------------" + DateTime.Now.Ticks.ToString("x"); + var boundary = "---------------------------" + DateTime.Now.Ticks.ToString("x"); request.ContentType = "multipart/form-data; boundary=" + boundary; - Stream requestStream = request.GetRequestStream(); + var requestStream = request.GetRequestStream(); if (options.TryGetValue("postData", out object postDataObject)) { var jsonPostData = (JObject)JsonConvert.DeserializeObject((string)postDataObject); - string formDataTemplate = "--{0}\r\nContent-Disposition: form-data; name=\"{1}\"\r\n\r\n{2}\r\n"; + const string formDataTemplate = "--{0}\r\nContent-Disposition: form-data; name=\"{1}\"\r\n\r\n{2}\r\n"; if (jsonPostData != null) { foreach (var data in jsonPostData) { - string item = string.Format(formDataTemplate, boundary, data.Key, data.Value); - byte[] itemBytes = Encoding.UTF8.GetBytes(item); - await requestStream.WriteAsync(itemBytes, 0, itemBytes.Length); + var item = string.Format(formDataTemplate, boundary, data.Key, data.Value); + var itemBytes = Encoding.UTF8.GetBytes(item); + await requestStream.WriteAsync(itemBytes); } } } var imageData = options["imageData"] as string; var matchingDimensions = options["matchingDimensions"] as bool? ?? false; - byte[] fileToUpload = Program.AppApiInstance.ResizeImageToFitLimits(Convert.FromBase64String(imageData), matchingDimensions); + var fileToUpload = Program.AppApiInstance.ResizeImageToFitLimits(Convert.FromBase64String(imageData), matchingDimensions); - string fileFormKey = "file"; - string fileName = "blob"; - string fileMimeType = "image/png"; - string HeaderTemplate = "--{0}\r\nContent-Disposition: form-data; name=\"{1}\"; filename=\"{2}\"\r\nContent-Type: {3}\r\n\r\n"; - string header = string.Format(HeaderTemplate, boundary, fileFormKey, fileName, fileMimeType); - byte[] headerbytes = Encoding.UTF8.GetBytes(header); - await requestStream.WriteAsync(headerbytes, 0, headerbytes.Length); - using (MemoryStream fileStream = new MemoryStream(fileToUpload)) + const string fileFormKey = "file"; + const string fileName = "blob"; + const string fileMimeType = "image/png"; + const string headerTemplate = "--{0}\r\nContent-Disposition: form-data; name=\"{1}\"; filename=\"{2}\"\r\nContent-Type: {3}\r\n\r\n"; + var header = string.Format(headerTemplate, boundary, fileFormKey, fileName, fileMimeType); + var headerBytes = Encoding.UTF8.GetBytes(header); + await requestStream.WriteAsync(headerBytes); + using (var fileStream = new MemoryStream(fileToUpload)) { - byte[] buffer = new byte[1024]; - int bytesRead = 0; + var buffer = new byte[1024]; + var bytesRead = 0; while ((bytesRead = fileStream.Read(buffer, 0, buffer.Length)) != 0) { - await requestStream.WriteAsync(buffer, 0, bytesRead); + await requestStream.WriteAsync(buffer.AsMemory(0, bytesRead)); } fileStream.Close(); } - byte[] newlineBytes = Encoding.UTF8.GetBytes("\r\n"); - await requestStream.WriteAsync(newlineBytes, 0, newlineBytes.Length); - byte[] endBytes = Encoding.UTF8.GetBytes("--" + boundary + "--"); - await requestStream.WriteAsync(endBytes, 0, endBytes.Length); + var newlineBytes = Encoding.UTF8.GetBytes("\r\n"); + await requestStream.WriteAsync(newlineBytes); + var endBytes = Encoding.UTF8.GetBytes("--" + boundary + "--"); + await requestStream.WriteAsync(endBytes); requestStream.Close(); } diff --git a/src/app.js b/src/app.js index 5d8c9b5a..ed4f786c 100644 --- a/src/app.js +++ b/src/app.js @@ -15453,8 +15453,7 @@ console.log(`isLinux: ${LINUX}`); API.getAvatarImages = function (params) { return this.call(`file/${params.fileId}`, { - method: 'GET', - params + method: 'GET' }).then((json) => { var args = { json,