Skip to content

Commit

Permalink
Auto Updater: Support any github repo
Browse files Browse the repository at this point in the history
  • Loading branch information
idlesauce authored Oct 4, 2024
1 parent 5742c03 commit 8c4bdb1
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 16 deletions.
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
# PS5-Exploit-Host-Self-Host-Tool

Easy way to self host [PS5-Exploit-Host](https://github.com/idlesauce/PS5-Exploit-Host/).
This program shouldn't be considered production-ready, expose it to the internet at your own risk!
If you want to self-host and expose it to the internet consider using actively maintained projects like PiHole for DNS and nginx for the static file hosting.
You can find the static files (optionally) already set up with appcache under the [releases tab of the PS5-Exploit-Host repo](https://github.com/idlesauce/PS5-Exploit-Host/releases/tag/latest).

## Features
*All features are customizable and can be disabled individually*
- **HTTPS file server**
- **Automatic updates** for served files from [PS5-Exploit-Host](https://github.com/idlesauce/PS5-Exploit-Host/)
- **Automatic updates** for served files
- By default using cached [PS5-Exploit-Host](https://github.com/idlesauce/PS5-Exploit-Host/).
- Configurable to work with any github repo, including a specific folder in it.
- More info in notes.
- **DNS server**
- Blocks all PlayStation domains except manuals.playstation.net, which redirects to your computer's IP.
- Option to block all other domains or specify a whitelist.
Expand Down Expand Up @@ -84,10 +86,23 @@ curl -s https://raw.githubusercontent.com/idlesauce/PS5-Exploit-Host-Self-Host-T
- Generally i would recommend using appcache without https and only using this program to refresh the cache.

- If you'd prefer to use the user guide, you can either set `HostFilesAutoUpdaterUseCachedVersion` to `false` in the `config.ini` which will fetch the files from github on the next start if needed. With this version you'll no longer get the error toast but this wont work offline.

- You can also keep using the user guide with cache after setting `BlockAppCacheRequestsOnHttps` to `false` in the `config.ini` but you'll have to do one of the following to update the cache:
- Run the appcache remover option in my host <!-- TODO: which works even in webkit-only mode. -->
- You can also clear the appcache by connecting to a network with internet and disabling the `UserGuideRedirect` option in the `config.ini` and restarting the app, after which you'll get served the real user guide with the trusted certificate which will clear the old appcache.

- The `HostFilesAutoUpdaterSource` field in the `config.ini` accepts the following values:
- `appcache` which downloads [PS5-Exploit-Host](https://github.com/idlesauce/PS5-Exploit-Host/) with appcache set up.
- `no-appcache` which downloads [PS5-Exploit-Host](https://github.com/idlesauce/PS5-Exploit-Host/) without appcache.
- Any link to a .zip, but the host must send an `ETag` header, since this is used to determine if the file changed without having to download the zip every time.
- A GitHub url in one of the following formats:
- `https://github.com/idlesauce/PS5-Exploit-Host`
- `https://github.com/idlesauce/PS5-Exploit-Host/tree/dev`
- `https://github.com/idlesauce/PS5-Exploit-Host/tree/main/document/en/ps5`
- `https://github.com/idlesauce/PS5-Exploit-Host/releases/download/latest/ps5-exploit-host_appcache.zip`

- The auto updater looks for an `index.html` in the archive and will only extract containing folder of the first match.

- To avoid ever using the default DNS, you can also set up the Wi-Fi connection manually through the `Set Up Manually` option in the bottom of the `Set Up Internet Connection` menu which allows you to set the DNS manually before the first connection


Expand Down
94 changes: 87 additions & 7 deletions src/AutoUpdater.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,60 @@ internal class AutoUpdateService : BackgroundService
private static readonly string lastDownloadedVersionFilePath = Path.Combine(Config.NonUserEditableFilesPath, "auto-updater-last-downloaded-version");
private readonly HttpClient httpClient;

private string toDownloadUrl => config.HostFilesAutoUpdaterUseCachedVersion ? cacheVersionUrl : nocacheVersionUrl;
private (string url, string? pathToFolderInDownloadedZip) downloadUrl
{
get
{
if (config.HostFilesAutoUpdaterSource == "appcache")
{
return (cacheVersionUrl, null);
}
else if (config.HostFilesAutoUpdaterSource == "no-appcache")
{
return (nocacheVersionUrl, null);
}

if (!Uri.TryCreate(config.HostFilesAutoUpdaterSource, UriKind.Absolute, out var parsedUrl))
{
throw new ArgumentException("Invalid HostFilesAutoUpdaterSource, must be 'appcache', 'no-appcache' or a .zip download link or github repo url", nameof(Config.HostFilesAutoUpdaterSource));
}

if (parsedUrl.AbsolutePath.EndsWith(".zip"))
{
return (config.HostFilesAutoUpdaterSource, null);
}

if (parsedUrl.Host != "github.com")
{
throw new ArgumentException("Invalid HostFilesAutoUpdaterSource, must be 'appcache', 'no-appcache' or a .zip download link or github repo url", nameof(Config.HostFilesAutoUpdaterSource));
}


var tree = "main";
if (parsedUrl.AbsolutePath.Contains("/tree/"))
{
tree = parsedUrl.AbsolutePath.Split("/tree/")[1].Split('/')[0];
}

var ownerSlashRepo = string.Join("/", parsedUrl.AbsolutePath.TrimStart('/').Split('/').Take(2));

string? pathToFolderInDownloadedZip = null;
if (parsedUrl.AbsolutePath.Contains("/tree/"))
{
var partAfterTree = parsedUrl.AbsolutePath.Split("/tree/")[1];
if (partAfterTree.Contains('/'))
{
var partAfterTreeTree = partAfterTree.Split('/')[1];
if (!string.IsNullOrWhiteSpace(partAfterTreeTree))
{
pathToFolderInDownloadedZip = $"{ownerSlashRepo.Split('/')[1]}-{tree}/{partAfterTreeTree}";
}
}
}

return ($"https://github.com/{ownerSlashRepo}/archive/refs/heads/{tree}.zip", pathToFolderInDownloadedZip);
}
}

private bool skipInitialTick = true;

Expand All @@ -42,7 +95,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
}
skipInitialTick = false;


string? lastModified = await GetLatestEtag();
if (lastModified is null)
{ continue; }
Expand All @@ -63,7 +116,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)

public async Task<string?> GetLatestEtag()
{
var url = config.HostFilesAutoUpdaterUseCachedVersion ? cacheVersionUrl : nocacheVersionUrl;
var (url, _) = downloadUrl;

var msg = new HttpRequestMessage(HttpMethod.Head, url);
HttpResponseMessage? response = null;
Expand All @@ -84,6 +137,15 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
}

var etag = response.Headers.FirstOrDefault(x => x.Key == HeaderNames.ETag).Value?.FirstOrDefault();
if (etag is null)
{
etag = response.Content.Headers.FirstOrDefault(x => x.Key == HeaderNames.ETag).Value?.FirstOrDefault();
}
if (etag is null)
{
throw new Exception("ETag not found in the response headers");
}

logger.LogTrace("Latest ETag: {etag}", etag);

return etag;
Expand All @@ -92,7 +154,8 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
public async Task DownloadLatestAndExtract()
{
logger.LogInformation("Downloading the latest release...");
var response = await httpClient.GetAsync(toDownloadUrl);
var (downloadUrl, pathToFolderInDownloadedZip) = this.downloadUrl;
var response = await httpClient.GetAsync(downloadUrl);
if (!response.IsSuccessStatusCode)
{
logger.LogError("Failed to download the latest release, status code: {StatusCode}", response.StatusCode);
Expand All @@ -108,7 +171,20 @@ public async Task DownloadLatestAndExtract()
// find index.html inside the zip file, and extract the folder containing it
using var archive = new ZipArchive(stream);
var allEntries = archive.Entries;
var indexHtmlEntry = allEntries.FirstOrDefault(entry => entry.FullName.EndsWith("index.html", StringComparison.InvariantCultureIgnoreCase));

ZipArchiveEntry? indexHtmlEntry = null;
foreach (var entry in allEntries)
{
if (!entry.FullName.EndsWith("index.html", StringComparison.InvariantCultureIgnoreCase))
{ continue; }

if (pathToFolderInDownloadedZip is not null && !entry.FullName.Replace('\\', '/').TrimStart('/').StartsWith(pathToFolderInDownloadedZip, StringComparison.InvariantCultureIgnoreCase))
{ continue; }

indexHtmlEntry = entry;
break;
}

if (indexHtmlEntry is null)
{
logger.LogError("index.html not found in the update zip file");
Expand Down Expand Up @@ -152,6 +228,10 @@ public async Task DownloadLatestAndExtract()
// Save the etag
var etag = response.Headers.FirstOrDefault(x => x.Key == HeaderNames.ETag).Value?.FirstOrDefault();
if (etag is null)
{
etag = response.Content.Headers.FirstOrDefault(x => x.Key == HeaderNames.ETag).Value?.FirstOrDefault();
}
if (etag is null)
{
// This would make it so it downloads the update every tick so abort
// but this shouldnt be reached
Expand All @@ -160,7 +240,7 @@ public async Task DownloadLatestAndExtract()

await File.WriteAllTextAsync(lastEtagFilePath, etag);

await File.WriteAllTextAsync(lastDownloadedVersionFilePath, toDownloadUrl);
await File.WriteAllTextAsync(lastDownloadedVersionFilePath, downloadUrl);

skipInitialTick = false;
}
Expand All @@ -175,7 +255,7 @@ public async Task StartupFetchIfNeeded()
}

// if the user changed the cached/no-cache setting, download the latest release
if (!File.Exists(lastDownloadedVersionFilePath) || await File.ReadAllTextAsync(lastDownloadedVersionFilePath) != toDownloadUrl)
if (!File.Exists(lastDownloadedVersionFilePath) || await File.ReadAllTextAsync(lastDownloadedVersionFilePath) != downloadUrl.url)
{
await DownloadLatestAndExtract();
return;
Expand Down
36 changes: 29 additions & 7 deletions src/Config.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,29 @@ public class Config
[IniProperty(Comment = "Enable auto updates of the host files from github")]
public bool EnableHostFilesAutoUpdater { get; set; } = true;

[IniProperty(Comment = "The appcache will fail to update after a reboot because of the untrusted certificate, this will block the appcache requests on HTTPS")]
public bool BlockAppCacheRequestsOnHttps { get; set; } = true;

[IniProperty(Comment = "Dynamically generate the appcache file based on the files in the wwwroot folder and their modification times, you'll need to have the manifest file specified in the html for this to work")]
public bool DynamicallyGenerateAppcache { get; set; } = false;
private string _hostFilesAutoUpdaterSource = "appcache";
[IniProperty(Comment = "'appcache' or 'no-appcache' for PS5-Exploit-Host. Alternatively, set any .zip direct link (must have an ETag header) or set to any github repo url, optionally including a folder in the repo, to use/update host files from a custom source")]
public string HostFilesAutoUpdaterSource
{
get => _hostFilesAutoUpdaterSource;
set
{
if (value != "appcache" && value != "no-appcache" && !Uri.TryCreate(value, UriKind.Absolute, out var parsedUrl))
{
// we only accept github urls like this:
// - any link ending in .zip
// - https://github.com/idlesauce/PS5-Exploit-Host
// - https://github.com/idlesauce/PS5-Exploit-Host/tree/dev
// - https://github.com/idlesauce/PS5-Exploit-Host/tree/main/document/en/ps5

if (parsedUrl is null || (parsedUrl.Host != "github.com" && !parsedUrl.AbsolutePath.EndsWith(".zip")))
{
throw new ArgumentException("Invalid HostFilesAutoUpdaterSource, must be 'appcache', 'no-appcache' or a .zip download link or github repo url", nameof(HostFilesAutoUpdaterSource));
}
}
_hostFilesAutoUpdaterSource = value;
}
}

private int _hostFilesAutoUpdaterCheckIntervalMinutes = 60;
[IniProperty(Comment = "Interval in minutes to check github for updates to host files")]
Expand All @@ -69,8 +87,12 @@ public int HostFilesAutoUpdaterCheckIntervalMinutes
}
}

[IniProperty(Comment = "Host updater fetch version with offline appcache")]
public bool HostFilesAutoUpdaterUseCachedVersion { get; set; } = true;
[IniProperty(Comment = "The appcache will fail to update after a reboot because of the untrusted certificate, this will block the appcache requests on HTTPS")]
public bool BlockAppCacheRequestsOnHttps { get; set; } = true;

[IniProperty(Comment = "Dynamically generate the appcache file based on the files in the wwwroot folder and their modification times, you'll need to have the manifest file specified in the html for this to work")]
public bool DynamicallyGenerateAppcache { get; set; } = false;


[IniProperty(Comment = "[Windows & Linux only] Create a WiFi access point")]
public bool EnableWifiAP { get; set; } = false;
Expand Down
4 changes: 4 additions & 0 deletions src/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
{
Exception ex = (Exception)a.ExceptionObject;
Console.WriteLine($"Unhandled exception: {ex.GetType().Name}: {ex.Message}");
if (ex.InnerException is not null)
{
Console.WriteLine($"Inner exception: {ex.InnerException.GetType().Name}: {ex.InnerException.Message}");
}
Environment.Exit(1);
};

Expand Down

0 comments on commit 8c4bdb1

Please sign in to comment.