diff --git a/Program.cs b/Program.cs index 58c1f092..fabe38f1 100644 --- a/Program.cs +++ b/Program.cs @@ -60,11 +60,13 @@ namespace VRCX CpuMonitor.Instance.Init(); Discord.Instance.Init(); SQLite.Instance.Init(); + WebApi.Instance.Init(); LogWatcher.Instance.Init(); VRCXVR.Init(); Application.Run(new MainForm()); VRCXVR.Exit(); LogWatcher.Instance.Exit(); + WebApi.Instance.Exit(); SQLite.Instance.Exit(); Discord.Instance.Exit(); CpuMonitor.Instance.Exit(); diff --git a/Util.cs b/Util.cs index 06eb37c5..0d97cbbe 100644 --- a/Util.cs +++ b/Util.cs @@ -12,6 +12,7 @@ namespace VRCX }; repository.Register("VRCX", VRCX.Instance, true, options); repository.Register("SharedVariable", SharedVariable.Instance, false, options); + repository.Register("WebApi", WebApi.Instance, true, options); repository.Register("VRCXStorage", VRCXStorage.Instance, false, options); repository.Register("SQLite", SQLite.Instance, true, options); repository.Register("LogWatcher", LogWatcher.Instance, true, options); diff --git a/VRCX.csproj b/VRCX.csproj index 4b3b18a9..a490a2bb 100644 --- a/VRCX.csproj +++ b/VRCX.csproj @@ -131,6 +131,7 @@ + diff --git a/WebApi.cs b/WebApi.cs new file mode 100644 index 00000000..32eba673 --- /dev/null +++ b/WebApi.cs @@ -0,0 +1,149 @@ +using CefSharp; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Runtime.Serialization.Formatters.Binary; + +namespace VRCX +{ + public class WebApi + { + private readonly string COOKIE_FILE_NAME = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "cookies.dat"); + public static WebApi Instance { get; private set; } + private CookieContainer _cookieContainer; + + static WebApi() + { + Instance = new WebApi(); + ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12; + } + + public WebApi() + { + _cookieContainer = new CookieContainer(); + } + + internal void Init() + { + try + { + using (var file = File.Open(COOKIE_FILE_NAME, FileMode.Open, FileAccess.Read)) + { + _cookieContainer = (CookieContainer)new BinaryFormatter().Deserialize(file); + } + } + catch + { + } + } + + internal void Exit() + { + try + { + using (var file = File.Open(COOKIE_FILE_NAME, FileMode.Create, FileAccess.Write)) + { + new BinaryFormatter().Serialize(file, _cookieContainer); + } + } + catch + { + } + } + + public void ClearCookies() + { + _cookieContainer = new CookieContainer(); + } + +#pragma warning disable CS4014 + public async void Execute(IDictionary options, IJavascriptCallback callback) + { + try + { + var request = WebRequest.CreateHttp((string)options["url"]); + request.CookieContainer = _cookieContainer; + request.KeepAlive = true; + + if (options.TryGetValue("headers", out object headers) == true) + { + foreach (var header in (IEnumerable>)headers) + { + var key = header.Key; + var value = header.Value.ToString(); + + if (string.Compare(key, "Content-Type", StringComparison.OrdinalIgnoreCase) == 0) + { + request.ContentType = value; + } + else if (string.Compare(key, "User-Agent", StringComparison.OrdinalIgnoreCase) == 0) + { + request.UserAgent = value; + } + else + { + request.Headers.Add(key, value); + } + } + } + + if (options.TryGetValue("method", out object method) == true) + { + var _method = (string)method; + request.Method = _method; + + if (string.Compare(_method, "GET", StringComparison.OrdinalIgnoreCase) != 0 && + options.TryGetValue("body", out object body) == true) + { + using (var stream = await request.GetRequestStreamAsync()) + using (var streamWriter = new StreamWriter(stream)) + { + await streamWriter.WriteAsync((string)body); + } + } + } + + try + { + using (var response = await request.GetResponseAsync() as HttpWebResponse) + using (var stream = response.GetResponseStream()) + using (var streamReader = new StreamReader(stream)) + { + callback.ExecuteAsync(null, new + { + data = await streamReader.ReadToEndAsync(), + status = response.StatusCode + }); + } + } + catch (WebException webException) + { + if (webException.Response is HttpWebResponse response) + { + using (var stream = response.GetResponseStream()) + using (var streamReader = new StreamReader(stream)) + { + callback.ExecuteAsync(null, new + { + data = await streamReader.ReadToEndAsync(), + status = response.StatusCode + }); + } + } + else + { + callback.ExecuteAsync(webException.Message, null); + } + } + } + catch (Exception e) + { + callback.ExecuteAsync(e.Message, null); + } + + callback.Dispose(); + } +#pragma warning restore CS4014 + } +} diff --git a/html/src/app.js b/html/src/app.js index 9fc4a36b..5f8a809e 100644 --- a/html/src/app.js +++ b/html/src/app.js @@ -13,12 +13,13 @@ import locale from 'element-ui/lib/locale/lang/en'; import sharedRepository from './repository/shared.js'; import configRepository from './repository/config.js'; - window.sharedRepository = sharedRepository; window.configRepository = configRepository; +import webApiService from './service/webapi.js'; (async function () { await CefSharp.BindObjectAsync( + 'WebApi', 'VRCX', 'SharedVariable', // DO NOT DIRECT ACCESS 'VRCXStorage', @@ -330,30 +331,25 @@ window.configRepository = configRepository; API.pendingGetRequests = new Map(); API.call = function (endpoint, options) { - var resource = `https://api.vrchat.cloud/api/1/${endpoint}`; var init = { + url: `https://api.vrchat.cloud/api/1/${endpoint}`, method: 'GET', - mode: 'cors', - credentials: 'include', - cache: 'no-cache', - redirect: 'follow', - referrerPolicy: 'no-referrer', ...options }; var { params } = init; var isGetRequest = init.method === 'GET'; - if (isGetRequest) { + if (isGetRequest === true) { // transform body to url if (params === Object(params)) { - var url = new URL(resource); + var url = new URL(init.url); var { searchParams } = url; for (var key in params) { searchParams.set(key, params[key]); } - resource = url.toString(); + init.url = url.toString(); } // merge requests - var req = this.pendingGetRequests.get(resource); + var req = this.pendingGetRequests.get(init.url); if (req !== undefined) { return req; } @@ -366,45 +362,50 @@ window.configRepository = configRepository; ? JSON.stringify(params) : '{}'; } - var req = fetch(resource, init).catch((err) => { + var req = webApiService.execute(init).catch((err) => { this.$throw(0, err); - }).then((res) => res.json().catch(() => { - if (res.ok) { + }).then((response) => { + try { + response.data = JSON.parse(response.data); + return response; + } catch (e) { + } + if (response.status === 200) { this.$throw(0, 'Invalid JSON response'); } this.$throw(res.status); - }).then((json) => { - if (res.ok) { - if (json.success === Object(json.success)) { - new Noty({ - type: 'success', - text: escapeTag(json.success.message) - }).show(); + }).then(({ data, status }) => { + if (data === Object(data)) { + if (status === 200) { + if (data.success === Object(data.success)) { + new Noty({ + type: 'success', + text: escapeTag(data.success.message) + }).show(); + } + return data; } - return json; - } - if (json === Object(json)) { - if (json.error === Object(json.error)) { + if (data.error === Object(data.error)) { this.$throw( - json.error.status_code || res.status, - json.error.message, - json.error.data + data.error.status_code || status, + data.error.message, + data.error.data ); - } else if (typeof json.error === 'string') { + } else if (typeof data.error === 'string') { this.$throw( - json.status_code || res.status, - json.error + data.status_code || status, + data.error ); } } - this.$throw(res.status, json); - return json; - })); - if (isGetRequest) { + this.$throw(status, data); + return data; + }); + if (isGetRequest === true) { req.finally(() => { - this.pendingGetRequests.delete(resource); + this.pendingGetRequests.delete(init.url); }); - this.pendingGetRequests.set(resource, req); + this.pendingGetRequests.set(init.url, req); } return req; }; @@ -817,7 +818,7 @@ window.configRepository = configRepository; API.currentUser = {}; API.$on('LOGOUT', function () { - VRCX.DeleteAllCookies(); + webApiService.clearCookies(); this.isLoggedIn = false; }); @@ -3341,28 +3342,33 @@ window.configRepository = configRepository; return style; }; - $app.methods.checkAppVersion = function () { - var url = 'https://api.github.com/repos/pypy-vrc/VRCX/releases/latest'; - fetch(url).then((res) => res.json()).then((json) => { - if (json === Object(json) && - json.name && - json.published_at) { - this.latestAppVersion = `${json.name} (${formatDate(json.published_at, 'YYYY-MM-DD HH24:MI:SS')})`; - if (json.name > this.appVersion) { - new Noty({ - type: 'info', - text: `Update available!!
${this.latestAppVersion}`, - timeout: 60000, - callbacks: { - onClick: () => VRCX.OpenLink('https://github.com/pypy-vrc/VRCX/releases') - } - }).show(); - this.notifyMenu('more'); - } - } else { - this.latestAppVersion = 'Error occured'; + $app.methods.checkAppVersion = async function () { + var response = await webApiService.execute({ + url: 'https://api.github.com/repos/pypy-vrc/VRCX/releases/latest', + method: 'GET', + headers: { + 'User-Agent': 'VRCX' } }); + var json = JSON.parse(response.data); + if (json === Object(json) && + json.name && + json.published_at) { + this.latestAppVersion = `${json.name} (${formatDate(json.published_at, 'YYYY-MM-DD HH24:MI:SS')})`; + if (json.name > this.appVersion) { + new Noty({ + type: 'info', + text: `Update available!!
${this.latestAppVersion}`, + timeout: 60000, + callbacks: { + onClick: () => VRCX.OpenLink('https://github.com/pypy-vrc/VRCX/releases') + } + }).show(); + this.notifyMenu('more'); + } + } else { + this.latestAppVersion = 'Error occured'; + } }; $app.methods.updateLoop = function () { diff --git a/html/src/service/webapi.js b/html/src/service/webapi.js new file mode 100644 index 00000000..1031f005 --- /dev/null +++ b/html/src/service/webapi.js @@ -0,0 +1,26 @@ +// requires binding of WebApi + +class WebApiService { + clearCookies() { + return WebApi.ClearCookies(); + } + + execute(options) { + return new Promise((resolve, reject) => { + WebApi.Execute(options, (err, response) => { + if (err !== null) { + reject(err); + return; + } + resolve(response); + }); + }); + } +} + +var self = new WebApiService(); + +export { + self as default, + WebApiService +};