using System; using System.Globalization; using System.IO; using System.IO.Compression; using System.Linq; using System.Net; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; namespace IdleMafiaBot { static class Updater { internal static string LatestUrl = "https://api.github.com/repos/geenusername/IdleMafiaBot/releases/latest"; internal static string SiteUrl = "https://afkmacro.com/update.json"; internal static string PublicKey = "2ShnG78eNnuxKJe4LSFty84YKNyRIPGOgsiCSXmweHPn6G/h/bw3VhIm0J6pXRd70JjnxbPeL1WoeErZ9hFuKptFZpBfuzUHJ8C7e7Y/Gewvz8cEYX0jUtykpjLcBchuzQiQyWuj6nULAVtcHVlxTJHQuQYFbtAWqGRjbJ2zYxNvf62My9jL86od/GDWQV8Y2nD+EkzMu5oLccAEZIunUQ0BJGN7eiO/NaRd4m4r2zfNADGHNeOnQH43UDwMe9ZChNCWVx2KoetS+SHnxfo+P4j77j9J0M/Mi705a6LcPb5Kdty2/CDuG3MEfY0yrO6v6NSCk1ZmI1DHqHBJ7GweS1O0kliJhJo0hLq76nkz8kylmKHXQW2mh1ErL9I2UXy/Qv0Lb+5JXLcX5zgW7+Q2Bbh4E7TqO8809huFQCMtxy0gIe5svKpKx/G6pskMQXrpojzzwVJ/5wgwOyGotdH7KTQmoS7CNVNVzlVAeeCsof2XsAN+4QJkfvRo2mJMHAYlAQAB"; public sealed class Release { public Version Version; public string Tag = ""; public string ZipUrl; public string Sha256; public string From = "GitHub"; } static readonly Regex TagRx = new Regex("\"tag_name\"\\s*:\\s*\"v?([0-9]+(?:\\.[0-9]+){1,3})\""); static readonly Regex ZipRx = new Regex("\"browser_download_url\"\\s*:\\s*\"(https://[^\"]+?\\.zip)\"", RegexOptions.IgnoreCase); static readonly Regex ShaRx = new Regex(@"SHA-?256\s+of\s+\W{0,3}IdleMafiaBot\.exe\W{0,6}([0-9A-Fa-f]{64})(?![0-9A-Fa-f])", RegexOptions.IgnoreCase); internal static bool AllowedHost(Uri u) { if (u == null || u.Scheme != Uri.UriSchemeHttps) return false; string h = u.Host.ToLowerInvariant(); return h == "afkmacro.com" || h == "www.afkmacro.com" || h == "api.github.com" || h == "github.com" || h.EndsWith(".githubusercontent.com", StringComparison.Ordinal); } internal static Release Parse(string json) { var t = TagRx.Match(json ?? ""); Version v; if (!t.Success || !Version.TryParse(t.Groups[1].Value, out v)) return null; var r = new Release { Version = v, Tag = t.Groups[1].Value }; var z = ZipRx.Match(json); if (z.Success) r.ZipUrl = z.Groups[1].Value; var s = ShaRx.Match(json); if (s.Success) r.Sha256 = s.Groups[1].Value.ToUpperInvariant(); return r; } public static bool Newer(Release r) { return r != null && r.Version > Version.Parse(Program.Version); } public static Release Check() { string siteError; try { var s = Parse(Encoding.UTF8.GetString(Get(SiteUrl, 1 << 20, false))); if (s != null && s.ZipUrl != null && AllowedHost(new Uri(s.ZipUrl))) { s.From = "afkmacro.com"; return s; } siteError = "afkmacro.com had no version"; } catch (Exception e) { siteError = e.Message; } Release r; try { r = Parse(Encoding.UTF8.GetString(Get(LatestUrl, 1 << 20, true))); } catch (Exception e) { throw new Exception(siteError + "; " + e.Message); } if (r == null) throw new Exception("GitHub's answer had no version in it"); return r; } public static string Install(Release r, string exePath) { if (r == null || r.ZipUrl == null) return "the release has no download"; if (r.Sha256 == null) return "the release doesn't say its fingerprint (SHA-256), so the download can't be checked - download it from afkmacro.com or our Discord"; byte[] zip; try { zip = Get(r.ZipUrl, 30 << 20, false); } catch (Exception e) { return "couldn't download it (" + e.Message + ")"; } return InstallFrom(zip, r.Sha256, exePath); } internal static string InstallFrom(byte[] zip, string sha256, string exePath) { byte[] exe = null, readme = null, sig = null; try { using (var a = new ZipArchive(new MemoryStream(zip), ZipArchiveMode.Read)) foreach (var e in a.Entries) { if (e.Length > 20 << 20) continue; if (e.Name.Equals("IdleMafiaBot.exe", StringComparison.OrdinalIgnoreCase)) exe = Bytes(e); else if (e.Name.Equals("README.txt", StringComparison.OrdinalIgnoreCase)) readme = Bytes(e); else if (e.Name.Equals("IdleMafiaBot.exe.sig", StringComparison.OrdinalIgnoreCase) && e.Length <= 4096) sig = Bytes(e); } } catch (Exception e) { return "the download isn't a zip that can be read (" + e.Message + ")"; } if (exe == null || exe.Length < 2 || exe[0] != 'M' || exe[1] != 'Z') return "the download has no IdleMafiaBot.exe in it"; string got; using (var h = SHA256.Create()) got = string.Concat(h.ComputeHash(exe).Select(b => b.ToString("X2", CultureInfo.InvariantCulture))); if (!got.Equals(sha256, StringComparison.OrdinalIgnoreCase)) return "the downloaded exe isn't the one the release names (its fingerprint differs) - nothing was changed"; if (sig == null) return "the download isn't signed by the bot's maker - nothing was changed. Download it from afkmacro.com or our Discord"; if (!Signed(exe, sig, PublicKey)) return "the download's signature isn't the bot maker's - nothing was changed. Download it from afkmacro.com or our Discord"; string dir = Path.GetDirectoryName(exePath), name = Path.GetFileNameWithoutExtension(exePath); string fresh = Path.Combine(dir, name + ".new.exe"), old = OldCopy(exePath); try { File.WriteAllBytes(fresh, exe); if (File.Exists(old)) File.Delete(old); File.Move(exePath, old); } catch (Exception e) { try { File.Delete(fresh); } catch (Exception) { } return "couldn't write in the bot's folder (" + e.Message + ") - move the bot to a folder of your own, like Documents"; } try { File.Move(fresh, exePath); } catch (Exception e) { try { File.Move(old, exePath); } catch (Exception) { } try { File.Delete(fresh); } catch (Exception) { } return "couldn't put the new exe in place (" + e.Message + ")"; } if (readme != null) try { File.WriteAllBytes(Path.Combine(dir, "README.txt"), readme); } catch (Exception) { } return null; } internal static string Undo(string exePath) { string old = OldCopy(exePath), aside = Path.Combine(Path.GetDirectoryName(exePath), Path.GetFileNameWithoutExtension(exePath) + ".new.exe"); if (!File.Exists(old)) return "the old version isn't there any more"; try { if (File.Exists(aside)) File.Delete(aside); File.Move(exePath, aside); } catch (Exception e) { return "couldn't move the new version aside (" + e.Message + ")"; } try { File.Move(old, exePath); } catch (Exception e) { try { File.Move(aside, exePath); } catch (Exception) { } return "couldn't put the old version back (" + e.Message + ")"; } try { File.Delete(aside); } catch (Exception) { } return null; } internal static bool Signed(byte[] exe, byte[] sig, string publicKeyXml) { if (exe == null || sig == null || sig.Length == 0 || string.IsNullOrEmpty(publicKeyXml)) return false; try { using (var rsa = new RSACryptoServiceProvider()) { rsa.PersistKeyInCsp = false; rsa.FromXmlString(publicKeyXml); if (!rsa.PublicOnly || rsa.KeySize < 3072) return false; return rsa.VerifyData(exe, "SHA256", sig); } } catch (Exception) { return false; } } internal static string OldCopy(string exePath) { return Path.Combine(Path.GetDirectoryName(exePath), Path.GetFileNameWithoutExtension(exePath) + ".old.exe"); } static byte[] Bytes(ZipArchiveEntry e) { using (var s = e.Open()) using (var m = new MemoryStream()) { s.CopyTo(m); return m.ToArray(); } } static byte[] Get(string url, int max, bool api) { ServicePointManager.SecurityProtocol |= (SecurityProtocolType)3072; var u = new Uri(url); for (int hop = 0; hop < 5; hop++) { if (!AllowedHost(u)) throw new Exception("not afkmacro.com or GitHub: " + u.Host); string who = u.Host.EndsWith("afkmacro.com", StringComparison.OrdinalIgnoreCase) ? "afkmacro.com" : "GitHub"; var req = (HttpWebRequest)WebRequest.Create(u); req.AllowAutoRedirect = false; req.UserAgent = "IdleMafiaBot/" + Program.Version; if (api) req.Accept = "application/vnd.github+json"; req.Timeout = 20000; req.ReadWriteTimeout = 60000; HttpWebResponse resp; try { resp = (HttpWebResponse)req.GetResponse(); } catch (WebException e) { var r = e.Response as HttpWebResponse; if (r == null) throw new Exception("couldn't reach " + who + ": " + e.Message); using (r) throw new Exception(who + " answered " + (int)r.StatusCode + (r.StatusCode == (HttpStatusCode)403 && who == "GitHub" ? " (asked too often: try again in an hour)" : "")); } using (resp) { int code = (int)resp.StatusCode; if (code >= 300 && code < 400) { string to = resp.Headers["Location"]; if (string.IsNullOrEmpty(to)) throw new Exception(who + " sent it on without saying where"); u = new Uri(u, to); continue; } if (resp.ContentLength > max) throw new Exception("the file is too big"); using (var s = resp.GetResponseStream()) using (var m = new MemoryStream()) { var buf = new byte[81920]; int n; while ((n = s.Read(buf, 0, buf.Length)) > 0) { m.Write(buf, 0, n); if (m.Length > max) throw new Exception("the file is too big"); } return m.ToArray(); } } } throw new Exception("too many redirects"); } } }