diff --git a/AppApi.cs b/AppApi.cs index 97c1f3b1..229c2a84 100644 --- a/AppApi.cs +++ b/AppApi.cs @@ -16,6 +16,7 @@ using System.Security.Cryptography; using System.Net; using Windows.UI.Notifications; using Windows.Data.Xml.Dom; +using librsync.net; namespace VRCX { @@ -35,6 +36,22 @@ namespace VRCX return System.Convert.ToBase64String(md5); } + public string SignFile(string Blob) + { + byte[] fileData = Convert.FromBase64CharArray(Blob.ToCharArray(), 0, Blob.Length); + Stream sig = Librsync.ComputeSignature(new MemoryStream(fileData)); + var memoryStream = new MemoryStream(); + sig.CopyTo(memoryStream); + byte[] sigBytes = memoryStream.ToArray(); + return System.Convert.ToBase64String(sigBytes); + } + + public string FileLength(string Blob) + { + byte[] fileData = Convert.FromBase64CharArray(Blob.ToCharArray(), 0, Blob.Length); + return fileData.Length.ToString(); + } + public void ShowDevTools() { MainForm.Instance.Browser.ShowDevTools(); diff --git a/VRCX.csproj b/VRCX.csproj index 78816d01..62b2103b 100644 --- a/VRCX.csproj +++ b/VRCX.csproj @@ -77,6 +77,10 @@ + + False + librsync.net\librsync.net.dll + diff --git a/WebApi.cs b/WebApi.cs index efa7fc0c..147ea2a5 100644 --- a/WebApi.cs +++ b/WebApi.cs @@ -153,11 +153,11 @@ namespace VRCX } } - if (options.TryGetValue("uploadImagePUT", out object uploadImagePUT) == true) + if (options.TryGetValue("uploadFilePUT", out object uploadImagePUT) == true) { request.Method = "PUT"; - request.ContentType = "image/png"; - var imageData = options["imageData"] as string; + request.ContentType = options["fileMIME"] as string; + var imageData = options["fileData"] as string; byte[] sentData = Convert.FromBase64CharArray(imageData.ToCharArray(), 0, imageData.Length); request.ContentLength = sentData.Length; using (System.IO.Stream sendStream = request.GetRequestStream()) diff --git a/html/src/app.js b/html/src/app.js index d8ef5161..45f01a31 100644 --- a/html/src/app.js +++ b/html/src/app.js @@ -362,7 +362,7 @@ speechSynthesis.getVoices(); if (typeof req !== 'undefined') { return req; } - } else if (init.uploadImage || init.uploadImagePUT) { + } else if (init.uploadImage || init.uploadFilePUT) { } else { init.headers = { 'Content-Type': 'application/json;charset=utf-8', @@ -9965,11 +9965,21 @@ speechSynthesis.getVoices(); return a[field].toLowerCase().localeCompare(b[field].toLowerCase()); }; - $app.methods.md5 = async function (file) { + $app.methods.genMd5 = async function (file) { var response = await AppApi.MD5File(file); return response; }; + $app.methods.genSig = async function (file) { + var response = await AppApi.SignFile(file); + return response; + }; + + $app.methods.genLength = async function (file) { + var response = await AppApi.FileLength(file); + return response; + }; + $app.methods.onFileChangeAvatarImage = function (e) { var clearFile = function () { if (document.querySelector('#AvatarImageUploadButton')) { @@ -9989,9 +9999,9 @@ speechSynthesis.getVoices(); clearFile(); return; } - if (!files[0].type.match(/image.*/)) { + if (!files[0].type.match(/image.png/)) { $app.$message({ - message: 'File isn\'t an image', + message: 'File isn\'t a png', type: 'error' }); clearFile(); @@ -10000,27 +10010,30 @@ speechSynthesis.getVoices(); this.avatarDialog.loading = true; var r = new FileReader(); r.onload = async function (file) { - var base64Body = btoa(r.result); - var fileSize = file.total; - var md5 = await $app.md5(base64Body); + var base64File = btoa(r.result); + var fileMd5 = await $app.genMd5(base64File); + var fileSizeInBytes = file.total; + var base64SignatureFile = await $app.genSig(base64File); + var signatureMd5 = await $app.genMd5(base64SignatureFile); + var signatureSizeInBytes = await $app.genLength(base64SignatureFile); var avatarId = $app.avatarDialog.id; var { imageUrl } = $app.avatarDialog.ref; var url = new URL(imageUrl); var pathArray = url.pathname.split('/'); var fileId = pathArray[4]; - var signatureMd5 = await $app.md5(btoa(Math.random().toString(36).substring(7))); // lol... - var signatureSize = Math.floor(Math.random() * (10000 - 500 + 1)) + 500; $app.avatarImage = { - file: base64Body, - fileMd5: md5, - fileId: fileId, - avatarId: avatarId + base64File, + fileMd5, + base64SignatureFile, + signatureMd5, + fileId, + avatarId }; var params = { - fileMd5: md5, - fileSizeInBytes: fileSize, - signatureMd5: signatureMd5, - signatureSizeInBytes: signatureSize + fileMd5, + fileSizeInBytes, + signatureMd5, + signatureSizeInBytes }; API.uploadAvatarImage(params, fileId); }; @@ -10068,10 +10081,10 @@ speechSynthesis.getVoices(); }); var fileId = json.id; var fileVersion = json.versions[json.versions.length - 1].version; - await this.call(`file/${fileId}/${fileVersion}/signature/finish`, { + this.call(`file/${fileId}/${fileVersion}/signature/finish`, { method: 'PUT' }); - await this.call(`file/${fileId}/${fileVersion}/file/finish`, { + this.call(`file/${fileId}/${fileVersion}/file/finish`, { method: 'PUT' }); $app.avatarDialog.loading = false; @@ -10080,11 +10093,11 @@ speechSynthesis.getVoices(); API.$on('AVATARIMAGE:STAGE1', function (args) { var fileId = args.json.id; var fileVersion = args.json.versions[args.json.versions.length - 1].version; - var parmas = { + var params = { fileId, fileVersion }; - this.uploadAvatarImageStage2(parmas); + this.uploadAvatarImageStage2(params); }); API.uploadAvatarImageStage2 = async function (params) { @@ -10108,19 +10121,20 @@ speechSynthesis.getVoices(); API.$on('AVATARIMAGE:STAGE2', function (args) { var { url } = args.json; var { fileId, fileVersion } = args.params; - var parmas = { + var params = { url, fileId, fileVersion }; - this.uploadAvatarImageStage3(parmas); + this.uploadAvatarImageStage3(params); }); API.uploadAvatarImageStage3 = function (params) { return webApiService.execute({ url: params.url, - uploadImagePUT: true, - imageData: $app.avatarImage.file, + uploadFilePUT: true, + fileData: $app.avatarImage.base64File, + fileMIME: 'image/png', headers: { 'Content-MD5': $app.avatarImage.fileMd5 } @@ -10140,11 +10154,11 @@ speechSynthesis.getVoices(); API.$on('AVATARIMAGE:STAGE3', function (args) { var { fileId, fileVersion } = args.params; - var parmas = { + var params = { fileId, fileVersion }; - this.uploadAvatarImageStage4(parmas); + this.uploadAvatarImageStage4(params); }); API.uploadAvatarImageStage4 = function (params) { @@ -10166,14 +10180,75 @@ speechSynthesis.getVoices(); API.$on('AVATARIMAGE:STAGE4', function (args) { var { fileId, fileVersion } = args.params; - var parmas = { + var params = { fileId, fileVersion }; - this.uploadAvatarImageStage5(parmas); + this.uploadAvatarImageStage5(params); }); - API.uploadAvatarImageStage5 = function (params) { + API.uploadAvatarImageStage5 = async function (params) { + try { + return await this.call(`file/${params.fileId}/${params.fileVersion}/signature/start`, { + method: 'PUT' + }).then((json) => { + var args = { + json, + params + }; + this.$emit('AVATARIMAGE:STAGE5', args); + return args; + }); + } catch (err) { + console.error(err); + this.uploadAvatarFailCleanup(params.fileId); + } + }; + + API.$on('AVATARIMAGE:STAGE5', function (args) { + var { url } = args.json; + var { fileId, fileVersion } = args.params; + var params = { + url, + fileId, + fileVersion + }; + this.uploadAvatarImageStage6(params); + }); + + API.uploadAvatarImageStage6 = function (params) { + return webApiService.execute({ + url: params.url, + uploadFilePUT: true, + fileData: $app.avatarImage.base64SignatureFile, + fileMIME: 'application/x-rsync-signature', + headers: { + 'Content-MD5': $app.avatarImage.signatureMd5 + } + }).then((json) => { + if (json.status !== 200) { + $app.avatarDialog.loading = false; + this.$throw('Avatar image upload failed', json); + } + var args = { + json, + params + }; + this.$emit('AVATARIMAGE:STAGE6', args); + return args; + }); + }; + + API.$on('AVATARIMAGE:STAGE6', function (args) { + var { fileId, fileVersion } = args.params; + var params = { + fileId, + fileVersion + }; + this.uploadAvatarImageStage7(params); + }); + + API.uploadAvatarImageStage7 = function (params) { return this.call(`file/${params.fileId}/${params.fileVersion}/signature/finish`, { method: 'PUT', params: { @@ -10185,21 +10260,21 @@ speechSynthesis.getVoices(); json, params }; - this.$emit('AVATARIMAGE:STAGE5', args); + this.$emit('AVATARIMAGE:STAGE7', args); return args; }); }; - API.$on('AVATARIMAGE:STAGE5', function (args) { + API.$on('AVATARIMAGE:STAGE7', function (args) { var { fileId, fileVersion } = args.params; var parmas = { id: $app.avatarImage.avatarId, imageUrl: `https://api.vrchat.cloud/api/1/file/${fileId}/${fileVersion}/file` }; - this.uploadAvatarImageStage6(parmas); + this.uploadAvatarImageStage8(parmas); }); - API.uploadAvatarImageStage6 = function (params) { + API.uploadAvatarImageStage8 = function (params) { return this.call(`avatars/${params.id}`, { method: 'PUT', params @@ -10208,13 +10283,13 @@ speechSynthesis.getVoices(); json, params }; - this.$emit('AVATARIMAGE:STAGE6', args); + this.$emit('AVATARIMAGE:STAGE8', args); this.$emit('AVATAR', args); return args; }); }; - API.$on('AVATARIMAGE:STAGE6', function (args) { + API.$on('AVATARIMAGE:STAGE8', function (args) { $app.avatarDialog.loading = false; if (args.json.imageUrl === args.params.imageUrl) { $app.$message({ diff --git a/html/src/index.pug b/html/src/index.pug index 8e6e1a87..28facf02 100644 --- a/html/src/index.pug +++ b/html/src/index.pug @@ -1786,6 +1786,30 @@ html OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. div(style="margin-top:15px") + p(style="font-weight:bold") librsync.net + pre(style="font-size:12px;white-space:pre-line"). + The MIT License (MIT) + + Copyright (c) 2015 Brad Dodson + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + div(style="margin-top:15px") p(style="font-weight:bold") Newtonsoft.Json pre(style="font-size:12px;white-space:pre-line"). The MIT License (MIT) diff --git a/librsync.net/librsync.net.dll b/librsync.net/librsync.net.dll new file mode 100644 index 00000000..0324aff5 Binary files /dev/null and b/librsync.net/librsync.net.dll differ