diff --git a/IPA.Tests/updater_test.json b/IPA.Tests/updater_test.json
index 36fca4a0..4f2ab017 100644
--- a/IPA.Tests/updater_test.json
+++ b/IPA.Tests/updater_test.json
@@ -15,32 +15,12 @@
"conflictsWith": [],
"files": {
"steam": {
- "hash": "b38f5f58b0131e10d4676cc94dad5315e2999aa8",
+ "hash": "a94e7eea2f656b2830a86000ee222b6cb06f8da5",
"files": {
- "CustomSongs/One More Time/": "da39a3ee5e6b4b0d3255bfef95601890afd80709",
- "CustomSongs/One More Time/cover.jpg": "95dbaecba1ac8165f4cd0a83950a31859931db27",
- "CustomSongs/One More Time/Credits.txt": "d067a9f5e0e7c6301dd9f6e8e7a609e82815345c",
- "CustomSongs/One More Time/Easy.json": "e93e234a6fd38afc3d216e6d0e1585f19cb49f69",
- "CustomSongs/One More Time/Expert.json": "8bd7f7e0e0782dce7b77a3bc8bdbc091f2fd202f",
- "CustomSongs/One More Time/Hard.json": "99e693c77d280c37a6cfd9f479f158cd6c84a354",
- "CustomSongs/One More Time/info.json": "d214d1990af6debf38ccfdb7c57462926fb0995c",
- "CustomSongs/One More Time/lyrics.srt": "b3ed8a5f4fc3595fb097943379fc8ea22548bd51",
- "CustomSongs/One More Time/Normal.json": "0f7c68b29dfb8c5cfde34b0dc3238353e2f9b24b",
- "CustomSongs/One More Time/One More Time.ogg": "4d74e9a58341439299512dce2155c97a00d3fb3b",
- "IPA.exe": "57521373f240845798bb99723ceb19ee09774131",
- "IPA/Backups/": "da39a3ee5e6b4b0d3255bfef95601890afd80709",
- "IPA/Data/": "da39a3ee5e6b4b0d3255bfef95601890afd80709",
- "IPA/Data/Managed/": "da39a3ee5e6b4b0d3255bfef95601890afd80709",
- "IPA/Data/Managed/IllusionInjector.dll": "b7f05f3c7abc052fab29787e97dfa5533ce8ff7d",
- "IPA/Data/Managed/IllusionInjector.pdb": "a4ca3e2aada545eae600a6b8971ca892682b6693",
- "IPA/Data/Managed/IllusionPlugin.dll": "c715be1506e16367c6cdb1c8e7b8bba845233f0f",
- "IPA/Data/Managed/IllusionPlugin.pdb": "516f73cc420bfcdcd38a991c7664d62d7deb745a",
- "IPA/Data/Managed/IllusionPlugin.xml": "e79c7cd5bc5369056d262fd963451e4fb22f8021",
- "IPA/Launcher.exe": "76a68378dd0ef1fe660b87b96b18c5e77709e3ff",
- "Mono.Cecil.dll": "762fb07e4d81722f0a766460289c6114cd4b7dae",
- "Plugins/SongLoaderPlugin.dll": "f4ab080fbcc5abc6005a868cd965920bac46ddaf"
+ "Plugins/": "da39a3ee5e6b4b0d3255bfef95601890afd80709",
+ "Plugins/RandomSong.dll": "64ee1ecfeda73c550004bdb5123c7ac47a45e428"
},
- "url": "file://Z:/Users/aaron/Source/Repos/IPA-Reloaded-BeatSaber/IPA/bin/Debug/Debug.zip"
+ "url": "https://www.modsaber.ml/cdn/randomsong/1.0.0-default.zip"
}
},
"weight": 7,
diff --git a/IPA/PatchContext.cs b/IPA/PatchContext.cs
index 29f2508c..8a2ab808 100644
--- a/IPA/PatchContext.cs
+++ b/IPA/PatchContext.cs
@@ -20,6 +20,7 @@ namespace IPA
public string DataPathDst { get; private set; }
public string ManagedPath { get; private set; }
public string EngineFile { get; private set; }
+ public string EngineWebRequestFile { get; private set; }
public string AssemblyFile { get; private set; }
public string[] Args { get; private set; }
public string ProjectRoot { get; private set; }
diff --git a/IllusionInjector/IllusionInjector.csproj b/IllusionInjector/IllusionInjector.csproj
index be93c80d..d4a58b08 100644
--- a/IllusionInjector/IllusionInjector.csproj
+++ b/IllusionInjector/IllusionInjector.csproj
@@ -25,6 +25,7 @@
prompt
4
false
+ true
none
@@ -80,8 +81,7 @@
-
-
+
diff --git a/IllusionInjector/Updating/ModsaberML/ApiEndpoint.cs b/IllusionInjector/Updating/ModsaberML/ApiEndpoint.cs
index 8a48b8eb..94fdaa0f 100644
--- a/IllusionInjector/Updating/ModsaberML/ApiEndpoint.cs
+++ b/IllusionInjector/Updating/ModsaberML/ApiEndpoint.cs
@@ -1,4 +1,5 @@
-using SimpleJSON;
+using IllusionInjector.Utilities;
+using SimpleJSON;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -25,8 +26,16 @@ namespace IllusionInjector.Updating.ModsaberML
public string Title;
public Version GameVersion;
public string Author;
- public string SteamFile = null;
- public string OculusFile = null;
+
+ public class PlatformFile
+ {
+ public byte[] Hash = new byte[20]; // 20 byte because sha1 is fucky
+ public Dictionary FileHashes = new Dictionary();
+ public string DownloadPath = null;
+ }
+
+ public PlatformFile SteamFile = null;
+ public PlatformFile OculusFile = null;
public static Mod DecodeJSON(JSONObject obj)
{
@@ -43,10 +52,19 @@ namespace IllusionInjector.Updating.ModsaberML
foreach (var item in obj["files"])
{
var key = item.Key;
+ var pfile = new PlatformFile()
+ {
+ DownloadPath = item.Value["url"],
+ Hash = LoneFunctions.StringToByteArray(item.Value["hash"])
+ };
+
+ foreach (var file in item.Value["files"])
+ pfile.FileHashes.Add(file.Key, LoneFunctions.StringToByteArray(file.Value));
+
if (key == "steam")
- outp.SteamFile = item.Value["url"];
+ outp.SteamFile = pfile;
if (key == "oculus")
- outp.OculusFile = item.Value["url"];
+ outp.OculusFile = pfile;
}
return outp;
diff --git a/IllusionInjector/Updating/ModsaberML/Updater.cs b/IllusionInjector/Updating/ModsaberML/Updater.cs
index b430a22d..a2b2cdcf 100644
--- a/IllusionInjector/Updating/ModsaberML/Updater.cs
+++ b/IllusionInjector/Updating/ModsaberML/Updater.cs
@@ -6,6 +6,7 @@ using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
@@ -150,29 +151,29 @@ namespace IllusionInjector.Updating.ModsaberML
StartCoroutine(UpdateModCoroutine(tempDirectory, item));
}
}
-
- public class StreamDownloadHandler : DownloadHandlerScript
+
+ class StreamDownloadHandler : DownloadHandlerScript
{
- public BlockingStream Stream { get; set; }
- public StreamDownloadHandler(BlockingStream stream)
+ public MemoryStream Stream { get; set; }
+
+ public StreamDownloadHandler(MemoryStream stream) : base()
{
Stream = stream;
}
- protected void ReceiveContentLength(long contentLength)
+ protected override void ReceiveContentLength(int contentLength)
{
- //(Stream.BaseStream as MemoryStream).Capacity = (int)contentLength;
+ Stream.Capacity = contentLength;
Logger.log.Debug($"Got content length: {contentLength}");
}
- protected void OnContentComplete()
+ protected override void CompleteContent()
{
- Stream.Open = false;
Logger.log.Debug("Download complete");
}
- protected bool ReceiveData(byte[] data, long dataLength)
+ protected override bool ReceiveData(byte[] data, int dataLength)
{
Logger.log.Debug("ReceiveData");
if (data == null || data.Length < 1)
@@ -181,7 +182,7 @@ namespace IllusionInjector.Updating.ModsaberML
return false;
}
- Stream.Write(data, 0, (int)dataLength);
+ Stream.Write(data, 0, dataLength);
return true;
}
@@ -199,19 +200,54 @@ namespace IllusionInjector.Updating.ModsaberML
}
- private void DownloadPluginAsync(BlockingStream stream, UpdateStruct item, string tempdir)
+ private void ExtractPluginAsync(MemoryStream stream, UpdateStruct item, ApiEndpoint.Mod.PlatformFile fileInfo)
{
-
Logger.log.Debug($"Getting ZIP file for {item.plugin.Plugin.Name}");
//var stream = await httpClient.GetStreamAsync(url);
- using (var zipFile = new ZipInputStream(stream))
+ var data = stream.GetBuffer();
+ SHA1 sha = new SHA1CryptoServiceProvider();
+ var hash = sha.ComputeHash(data);
+ if (!LoneFunctions.UnsafeCompare(hash, fileInfo.Hash))
+ throw new Exception("The hash for the file doesn't match what is defined");
+
+ using (var zipFile = ZipFile.Read(stream))
{
Logger.log.Debug("Streams opened");
- ZipEntry entry;
- while ((entry = zipFile.GetNextEntry()) != null)
+ foreach (var entry in zipFile)
{
Logger.log.Debug(entry?.FileName ?? "NULL");
+
+ if (entry.IsDirectory)
+ {
+ Logger.log.Debug($"Creating directory {entry.FileName}");
+ Directory.CreateDirectory(Path.Combine(Environment.CurrentDirectory, entry.FileName));
+ }
+ else
+ {
+ using (var ostream = new MemoryStream((int)entry.UncompressedSize))
+ {
+ entry.Extract(ostream);
+ ostream.Seek(0, SeekOrigin.Begin);
+
+ sha = new SHA1CryptoServiceProvider();
+ var fileHash = sha.ComputeHash(ostream);
+ if (!LoneFunctions.UnsafeCompare(fileHash, fileInfo.FileHashes[entry.FileName]))
+ throw new Exception("The hash for the file doesn't match what is defined");
+
+ ostream.Seek(0, SeekOrigin.Begin);
+ FileInfo targetFile = new FileInfo(Path.Combine(Environment.CurrentDirectory, entry.FileName));
+ if (targetFile.Exists)
+ {
+ Logger.log.Debug($"Target file {targetFile.FullName} exists");
+ }
+
+ var fstream = targetFile.Create();
+ ostream.CopyTo(fstream);
+
+ Logger.log.Debug($"Wrote file {targetFile.FullName}");
+ }
+ }
}
}
@@ -220,59 +256,75 @@ namespace IllusionInjector.Updating.ModsaberML
IEnumerator UpdateModCoroutine(string tempdir, UpdateStruct item)
{
-
- string url;
+ ApiEndpoint.Mod.PlatformFile platformFile;
if (SteamCheck.IsAvailable || item.externInfo.OculusFile == null)
- url = item.externInfo.SteamFile;
+ platformFile = item.externInfo.SteamFile;
else
- url = item.externInfo.OculusFile;
+ platformFile = item.externInfo.OculusFile;
+
+ string url = platformFile.DownloadPath;
Logger.log.Debug($"URL = {url}");
-
- using (var memStream = new EchoStream())
- using (var stream = new BlockingStream(memStream))
- using (var request = UnityWebRequest.Get(url))
- using (var taskTokenSource = new CancellationTokenSource())
+
+ const int MaxTries = 3;
+ int maxTries = MaxTries;
+ while (maxTries > 0)
{
- var dlh = new StreamDownloadHandler(stream);
- request.downloadHandler = dlh;
+ if (maxTries-- != MaxTries)
+ Logger.log.Info($"Re-trying download...");
- var downloadTask = Task.Run(() =>
- { // use slightly more multithreaded approach than coroutines
- DownloadPluginAsync(stream, item, tempdir);
- }, taskTokenSource.Token);
+ using (var stream = new MemoryStream())
+ using (var request = UnityWebRequest.Get(url))
+ using (var taskTokenSource = new CancellationTokenSource())
+ {
+ var dlh = new StreamDownloadHandler(stream);
+ request.downloadHandler = dlh;
- Logger.log.Debug("Sending request");
- Logger.log.Debug(request?.downloadHandler?.ToString() ?? "DLH==NULL");
- yield return request.SendWebRequest();
- Logger.log.Debug("Download finished");
+ Logger.log.Debug("Sending request");
+ //Logger.log.Debug(request?.downloadHandler?.ToString() ?? "DLH==NULL");
+ yield return request.SendWebRequest();
+ Logger.log.Debug("Download finished");
- if (stream.Open)
- { // anti-hang
- Logger.log.Warn("Downloader failed to call DownloadHandler");
- stream.Open = false; // no more writing
- stream.BaseStream.Write(new byte[] { 0 }, 0, 1);
- }
+ if (request.isNetworkError)
+ {
+ Logger.log.Error("Network error while trying to update mod");
+ Logger.log.Error(request.error);
+ taskTokenSource.Cancel();
+ continue;
+ }
+ if (request.isHttpError)
+ {
+ Logger.log.Error($"Server returned an error code while trying to update mod");
+ Logger.log.Error(request.error);
+ taskTokenSource.Cancel();
+ continue;
+ }
- if (request.isNetworkError)
- {
- Logger.log.Error("Network error while trying to update mod");
- Logger.log.Error(request.error);
- taskTokenSource.Cancel();
- yield break;
- }
- if (request.isHttpError)
- {
- Logger.log.Error($"Server returned an error code while trying to update mod");
- Logger.log.Error(request.error);
- taskTokenSource.Cancel();
- yield break;
- }
+ stream.Seek(0, SeekOrigin.Begin); // reset to beginning
- downloadTask.Wait(); // wait for the damn thing to finish
+ var downloadTask = Task.Run(() =>
+ { // use slightly more multithreaded approach than coroutines
+ ExtractPluginAsync(stream, item, platformFile);
+ }, taskTokenSource.Token);
+
+ while (!(downloadTask.IsCompleted || downloadTask.IsCanceled || downloadTask.IsFaulted))
+ yield return null; // pause coroutine until task is done
+
+ if (downloadTask.IsFaulted)
+ {
+ Logger.log.Error($"Error downloading mod {item.plugin.Plugin.Name}");
+ Logger.log.Error(downloadTask.Exception);
+ continue;
+ }
+
+ break;
+ }
}
- yield return null;
+ if (maxTries == 0)
+ Logger.log.Warn($"Plugin download failed {MaxTries} times, not re-trying");
+ else
+ Logger.log.Debug("Download complete");
}
}
}
diff --git a/IllusionInjector/Utilities/BlockingStream.cs b/IllusionInjector/Utilities/BlockingStream.cs
deleted file mode 100644
index e23811bb..00000000
--- a/IllusionInjector/Utilities/BlockingStream.cs
+++ /dev/null
@@ -1,88 +0,0 @@
-using IllusionInjector.Logging;
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.IO;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace IllusionInjector.Utilities
-{
- class BlockingStream : Stream
- {
- public BlockingStream(Stream bstr)
- {
- BaseStream = bstr;
- }
-
- public Stream BaseStream { get; set; }
-
- private bool _open = true;
- public bool Open {
- get
- {
- return CanWrite;
- }
- set
- {
- if (!_open)
- throw new InvalidOperationException("Blocking stream has already been closed!");
- else
- _open = value;
- }
- }
-
- private bool canReadOverride = true;
- public override bool CanRead => BaseStream.CanRead && canReadOverride;
-
- public override bool CanSeek => BaseStream.CanSeek;
-
- public override bool CanWrite => BaseStream.CanWrite && _open;
-
- public override long Length => BaseStream.Length;
-
- public override long Position { get => BaseStream.Position; set => BaseStream.Position = value; }
-
- public override void Flush()
- {
- BaseStream.Flush();
- }
-
- public override int Read(byte[] buffer, int offset, int count)
- {
- var read = 0;
- while (read < count && Open)
- {
- read += BaseStream.Read(buffer, read, count-read);
- }
-
- if (read == 0)
- {
- canReadOverride = false;
- }
-
- return read;
- }
-
- public override long Seek(long offset, SeekOrigin origin)
- {
- return BaseStream.Seek(offset, origin);
- }
-
- public override void SetLength(long value)
- {
- BaseStream.SetLength(value);
- }
-
- public override void Write(byte[] buffer, int offset, int count)
- {
- BaseStream.Write(buffer, offset, count);
- }
-
- public override string ToString()
- {
- return $"{base.ToString()} ({BaseStream?.ToString()})";
- }
- }
-}
diff --git a/IllusionInjector/Utilities/EchoStream.cs b/IllusionInjector/Utilities/EchoStream.cs
deleted file mode 100644
index 6d1a3ca0..00000000
--- a/IllusionInjector/Utilities/EchoStream.cs
+++ /dev/null
@@ -1,40 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace IllusionInjector.Utilities
-{
- public class EchoStream : MemoryStream
- {
- private ManualResetEvent m_dataReady = new ManualResetEvent(false);
- private byte[] m_buffer;
- private int m_offset;
- private int m_count;
-
- public override void Write(byte[] buffer, int offset, int count)
- {
- m_buffer = buffer;
- m_offset = offset;
- m_count = count;
- m_dataReady.Set();
- }
-
- public override int Read(byte[] buffer, int offset, int count)
- {
- if (m_buffer == null)
- {
- // Block until the stream has some more data.
- m_dataReady.Reset();
- m_dataReady.WaitOne();
- }
-
- Buffer.BlockCopy(m_buffer, m_offset, buffer, offset, (count < m_count) ? count : m_count);
- m_buffer = null;
- return (count < m_count) ? count : m_count;
- }
- }
-}
diff --git a/IllusionInjector/Utilities/LoneFunctions.cs b/IllusionInjector/Utilities/LoneFunctions.cs
new file mode 100644
index 00000000..1dad987f
--- /dev/null
+++ b/IllusionInjector/Utilities/LoneFunctions.cs
@@ -0,0 +1,42 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace IllusionInjector.Utilities
+{
+ public static class LoneFunctions
+ {
+ public static byte[] StringToByteArray(string hex)
+ {
+ int NumberChars = hex.Length;
+ byte[] bytes = new byte[NumberChars / 2];
+ for (int i = 0; i < NumberChars; i += 2)
+ bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
+ return bytes;
+ }
+
+ // Copyright (c) 2008-2013 Hafthor Stefansson
+ // Distributed under the MIT/X11 software license
+ // Ref: http://www.opensource.org/licenses/mit-license.php.
+ // From: https://stackoverflow.com/a/8808245/3117125
+ public static unsafe bool UnsafeCompare(byte[] a1, byte[] a2)
+ {
+ if (a1 == a2) return true;
+ if (a1 == null || a2 == null || a1.Length != a2.Length)
+ return false;
+ fixed (byte* p1 = a1, p2 = a2)
+ {
+ byte* x1 = p1, x2 = p2;
+ int l = a1.Length;
+ for (int i = 0; i < l / 8; i++, x1 += 8, x2 += 8)
+ if (*((long*)x1) != *((long*)x2)) return false;
+ if ((l & 4) != 0) { if (*((int*)x1) != *((int*)x2)) return false; x1 += 4; x2 += 4; }
+ if ((l & 2) != 0) { if (*((short*)x1) != *((short*)x2)) return false; x1 += 2; x2 += 2; }
+ if ((l & 1) != 0) if (*((byte*)x1) != *((byte*)x2)) return false;
+ return true;
+ }
+ }
+ }
+}
diff --git a/IllusionInjector/obj/Debug/IllusionInjector.csproj.CoreCompileInputs.cache b/IllusionInjector/obj/Debug/IllusionInjector.csproj.CoreCompileInputs.cache
index c8be9064..8446ea99 100644
--- a/IllusionInjector/obj/Debug/IllusionInjector.csproj.CoreCompileInputs.cache
+++ b/IllusionInjector/obj/Debug/IllusionInjector.csproj.CoreCompileInputs.cache
@@ -1 +1 @@
-c0ee92dfbaba38e571471f2b468aeb2a80b0f76b
+53986b9c1e3d7933198e1551a3ec82914a34cbb2