From 816d9749a3851ae59d8b8bfbac89613d768c134e Mon Sep 17 00:00:00 2001 From: Natsumi Date: Fri, 2 Apr 2021 00:28:43 +1300 Subject: [PATCH] Avatar image upload signature --- AppApi.cs | 17 ++++ VRCX.csproj | 4 + WebApi.cs | 6 +- html/src/app.js | 147 +++++++++++++++++++++++++--------- html/src/index.pug | 24 ++++++ librsync.net/librsync.net.dll | Bin 0 -> 22016 bytes 6 files changed, 159 insertions(+), 39 deletions(-) create mode 100644 librsync.net/librsync.net.dll 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 0000000000000000000000000000000000000000..0324aff5d07f4459d5499ef026efca8af34e7d2f GIT binary patch literal 22016 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 VO#fvJ>&^dO4gN