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