Skip to content

Commit

Permalink
Check SHA256 hashes, Significantly improve error handling
Browse files Browse the repository at this point in the history
- Fixed bug with Trace not writing exceptions due to the file being
  closed.
- Handles invalid hash, network connectivity, and general exceptions
  now.

Closes #7 and #35.
  • Loading branch information
fifty-six committed Feb 27, 2022
1 parent 0300645 commit d144bf1
Show file tree
Hide file tree
Showing 9 changed files with 213 additions and 54 deletions.
1 change: 1 addition & 0 deletions Scarab.Tests/DatabaseTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ public void Serialization()
new Version(3, 0, 0, 0),
new[] { "Vasi" },
"https://github.com/fifty-six/HollowKnight.QoL/releases/download/v3/QoL.zip",
string.Empty,
"QoL",
"A collection of various quality of life improvements."
),
Expand Down
1 change: 1 addition & 0 deletions Scarab.Tests/ModSourceTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public async Task Record()
new Version("1.3.2.2"),
Array.Empty<string>(),
string.Empty,
string.Empty,
"test",
"test"
);
Expand Down
2 changes: 1 addition & 1 deletion Scarab/Interfaces/IModDatabase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ public interface IModDatabase
{
IEnumerable<ModItem> Items { get; }

(string Url, int Version) Api { get; }
(string Url, int Version, string SHA256) Api { get; }
}
}
49 changes: 33 additions & 16 deletions Scarab/Models/ModItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@ public ModItem
Version version,
string[] dependencies,
string link,
string shasum,
string name,
string description
)
{
_state = state;

Sha256 = shasum;
Version = version;
Dependencies = dependencies;
Link = link;
Expand All @@ -34,6 +36,7 @@ string description
public Version Version { get; }
public string[] Dependencies { get; }
public string Link { get; }
public string Sha256 { get; }
public string Name { get; }
public string Description { get; }
public string DependenciesDesc { get; }
Expand Down Expand Up @@ -95,30 +98,44 @@ string description

public async Task OnInstall(IInstaller inst, Action<ModProgressArgs> setProgress)
{
if (State is InstalledState(var enabled, var updated))
ModState origState = State;

try
{
// If we're not updated, update
if (!updated)
if (State is InstalledState(var enabled, var updated))
{
await inst.Install(this, setProgress, enabled);
// If we're not updated, update
if (!updated)
{
await inst.Install(this, setProgress, enabled);
}
// Otherwise the user wanted to uninstall.
else
{
await inst.Uninstall(this);
}
}
// Otherwise the user wanted to uninstall.
else
{
await inst.Uninstall(this);
}
}
else
{
State = (NotInstalledState) State with { Installing = true };
State = (NotInstalledState) State with { Installing = true };

setProgress(new ModProgressArgs());
setProgress(new ModProgressArgs());

await inst.Install(this, setProgress, true);
await inst.Install(this, setProgress, true);

setProgress(new ModProgressArgs {
Completed = true
});
setProgress
(
new ModProgressArgs
{
Completed = true
}
);
}
}
catch
{
State = origState;
throw;
}
}

Expand Down
28 changes: 17 additions & 11 deletions Scarab/Models/ModLinks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,17 +92,23 @@ public override string ToString()
+ "}";
}

public string GetOSUrl()
public string SHA256 => Environment.OSVersion.Platform switch
{
return Environment.OSVersion.Platform switch
{
PlatformID.Win32NT => Windows.URL,
PlatformID.MacOSX => Mac.URL,
PlatformID.Unix => Linux.URL,

var val => throw new NotSupportedException(val.ToString())
};
}
PlatformID.Win32NT => Windows.SHA256,
PlatformID.MacOSX => Mac.SHA256,
PlatformID.Unix => Linux.SHA256,

var val => throw new NotSupportedException(val.ToString())
};

public string OSUrl => Environment.OSVersion.Platform switch
{
PlatformID.Win32NT => Windows.URL,
PlatformID.MacOSX => Mac.URL,
PlatformID.Unix => Linux.URL,

var val => throw new NotSupportedException(val.ToString())
};
}

public class Link
Expand All @@ -119,7 +125,7 @@ public override string ToString()
}
}

[Serializable]
// [Serializable]
public class ApiManifest
{
public int Version { get; set; }
Expand Down
6 changes: 4 additions & 2 deletions Scarab/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public static void Main(string[] args)

private static void SetupLogging()
{
using var fileListener = new TextWriterTraceListener
var fileListener = new TextWriterTraceListener
(
Path.Combine
(
Expand All @@ -39,6 +39,8 @@ private static void SetupLogging()
)
);

fileListener.TraceOutputOptions = TraceOptions.DateTime;

Trace.AutoFlush = true;

Trace.Listeners.Add(fileListener);
Expand Down Expand Up @@ -70,7 +72,7 @@ private static void WriteExceptionToLog(Exception e)
if (Debugger.IsAttached)
Debugger.Break();

Trace.WriteLine($"Caught exception, writing to log: {e.StackTrace}");
Trace.TraceError(e.ToString());

File.WriteAllText(dir + $"ModInstaller_Error_{date}.log", e.ToString());
File.WriteAllText(Path.Combine(Settings.GetOrCreateDirPath(), $"ModInstaller_Error_{date}.log"), e.ToString());
Expand Down
56 changes: 53 additions & 3 deletions Scarab/Services/Installer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Toolkit.HighPerformance;
Expand All @@ -15,6 +16,32 @@

namespace Scarab.Services
{
public class HashMismatchException : Exception
{
/// <summary>
/// The SHA256 value that was received
/// </summary>
public string Actual { get; }

/// <summary>
/// Expected SHA256 value
/// </summary>
public string Expected { get; }

/// <summary>
/// The name of the object being checked
/// </summary>
public string Name { get; }

public HashMismatchException(string name, string actual, string expected)
{
Name = name;
Actual = actual;
Expected = expected;
}

}

public class Installer : IInstaller
{
private enum Update
Expand Down Expand Up @@ -113,7 +140,7 @@ public async Task InstallApi()
}
}

private async Task _InstallApi((string Url, int Version) manifest)
private async Task _InstallApi((string Url, int Version, string SHA256) manifest)
{
bool was_vanilla = true;

Expand All @@ -124,12 +151,14 @@ private async Task _InstallApi((string Url, int Version) manifest)

was_vanilla = false;
}

(string api_url, int ver) = manifest;
(string api_url, int ver, string hash) = manifest;

string managed = _config.ManagedFolder;

(ArraySegment<byte> data, string _) = await DownloadFile(api_url, _ => { });

ThrowIfInvalidHash("the API", data, hash);

// Backup the vanilla assembly
if (was_vanilla)
Expand Down Expand Up @@ -181,6 +210,13 @@ private async Task _ToggleApi(Update update = Update.ForceUpdate)
await _InstallApi(_db.Api);
}

/// <summary>
/// Installs the given mod.
/// </summary>
/// <param name="mod">Mod to install</param>
/// <param name="setProgress">Action called to indicate progress asynchronously</param>
/// <param name="enable">Whether the mod is enabled after installation</param>
/// <exception cref="HashMismatchException">Thrown if the download doesn't match the given hash</exception>
public async Task Install(ModItem mod, Action<ModProgressArgs> setProgress, bool enable)
{
await InstallApi();
Expand Down Expand Up @@ -244,6 +280,8 @@ private async Task _Install(ModItem mod, Action<DownloadProgressArgs> setProgres

var (data, filename) = await DownloadFile(mod.Link, setProgress);

ThrowIfInvalidHash(mod.Name, data, mod.Sha256);

// Sometimes our filename is quoted, remove those.
filename = filename.Trim('"');

Expand Down Expand Up @@ -295,6 +333,18 @@ private async Task _Install(ModItem mod, Action<DownloadProgressArgs> setProgres
await _installed.RecordInstalledState(mod);
}

private static void ThrowIfInvalidHash(string name, ArraySegment<byte> data, string modSha256)
{
var sha = SHA256.Create();

byte[] hash = sha.ComputeHash(data.AsMemory().AsStream());

string strHash = BitConverter.ToString(hash).Replace("-", string.Empty);

if (!string.Equals(strHash, modSha256, StringComparison.OrdinalIgnoreCase))
throw new HashMismatchException(name, actual: strHash, expected: modSha256);
}

private async Task<(ArraySegment<byte> data, string filename)> DownloadFile(string uri, Action<DownloadProgressArgs> setProgress)
{
(ArraySegment<byte> bytes, HttpResponseMessage response) = await _hc.DownloadBytesWithProgressAsync(
Expand Down
7 changes: 4 additions & 3 deletions Scarab/Services/ModDatabase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public class ModDatabase : IModDatabase
private const string FALLBACK_MODLINKS_URI = "https://cdn.jsdelivr.net/gh/hk-modding/modlinks@latest/ModLinks.xml";
private const string FALLBACK_APILINKS_URI = "https://cdn.jsdelivr.net/gh/hk-modding/modlinks@latest/ApiLinks.xml";

public (string Url, int Version) Api { get; }
public (string Url, int Version, string SHA256) Api { get; }

public IEnumerable<ModItem> Items => _items;

Expand All @@ -31,9 +31,10 @@ private ModDatabase(IModSource mods, ModLinks ml, ApiLinks al)
{
var item = new ModItem
(
link: mod.Links.GetOSUrl(),
link: mod.Links.OSUrl,
version: mod.Version.Value,
name: mod.Name,
shasum: mod.Links.SHA256,
description: mod.Description,
dependencies: mod.Dependencies,

Expand All @@ -45,7 +46,7 @@ private ModDatabase(IModSource mods, ModLinks ml, ApiLinks al)

_items.Sort((a, b) => string.Compare(a.Name, b.Name));

Api = (al.Manifest.Links.GetOSUrl(), al.Manifest.Version);
Api = (al.Manifest.Links.OSUrl, al.Manifest.Version, al.Manifest.Links.SHA256);
}

public ModDatabase(IModSource mods, (ModLinks ml, ApiLinks al) links) : this(mods, links.ml, links.al) { }
Expand Down
Loading

0 comments on commit d144bf1

Please sign in to comment.