/// <summary> /// Construct a ModDirectoryEntry from the stored KV info. /// Thankfully, this one at least doesn't need crypto signing...I think /// </summary> /// <param name="des">The keyvalue object describing the ModDirectoryEntry.</param> public ModDirectoryEntry(KV.KeyValue des) { name = des.Key; foreach (KV.KeyValue kv in des.Children) { switch (kv.Key) { case "version": version = new Version(kv.GetString()); break; case "downloadurl": downloadurl = kv.GetString(); break; case "dependency": dependencies.Add(kv.GetString()); break; case "requiresource2": requiresource2 = kv.GetBool(); break; case "core": core = kv.GetBool(); break; default: // no default rule here break; } } }
/// <summary> /// Construct a ModDirectoryList from a file listing installed ModDirectories. /// </summary> /// <param name="descriptor"></param> public ModDirectoryList(KV.KeyValue descriptor) { foreach (KV.KeyValue node in descriptor.Children) { try { string name = node.Key; Uri directoryuri = null; Uri versionuri = null; foreach (KV.KeyValue prop in node.Children) { switch (prop.Key) { case "version": versionuri = new Uri(prop.GetString()); break; case "directory": directoryuri = new Uri(prop.GetString()); break; default: // ignore break; } } Uri uri = new Uri(node.GetString()); directories.Add(new Tuple <string, Uri, Uri>(name, directoryuri, versionuri)); } catch (Exception) { Console.WriteLine("Encountered a poor entry in the directory list, skipping..."); } } }
/// <summary> /// Checks gameinfo.txt (and later gameinfo.gi) to make sure that we are /// actually overriding the vpk, and our changes haven't been nuked by a /// "verify local game cache" use. /// </summary> public void CheckGameInfo() { KV.KeyValue gameinfo = null; if (File.Exists(dotapath + "/dota/gameinfo.txt")) { gameinfo = KV.KVParser.ParseKeyValueFile(dotapath + "/dota/gameinfo.txt"); } Console.WriteLine(gameinfo.ToString()); // Make sure our entry is there bool found = false; if (gameinfo != null && gameinfo["FileSystem"] != null && gameinfo["FileSystem"]["SearchPaths"] != null) { foreach (KV.KeyValue f in gameinfo["FileSystem"]["SearchPaths"].Children) { if (f.Key == "Game" && f.GetString() == "moddota") { found = true; break; } } } if (!found) { gameinfo = GetFixedGameInfo(); // Write to the file File.WriteAllText(dotapath + "/dota/gameinfo.txt", gameinfo.ToString()); } }
/// <summary> /// Checks the signature of a data file, and returns null if it fails, or the data if it succeeds. /// </summary> /// <param name="data">The file KV.</param> /// <returns>True if the signature check passes, false otherwise.</returns> public bool CheckSignature(KV.KeyValue data) { try { if (data["signatureinfo"] == null || data["body"] == null) { Console.WriteLine("Couldn't find the necessary signature information or body block of checked KV, failing signature check."); return(false); } if (data["signatureinfo"]["certificate"] == null) { Console.WriteLine("Couldn't find the certificate in the specified KV for signature check, failing signature check."); return(false); } if (data["signatureinfo"]["signature"] == null) { Console.WriteLine("Couldn't find the signature in the specifeid KV, failing signature check."); return(false); } X509Certificate2 certificate = new X509Certificate2(Convert.FromBase64String(data["signatureinfo"]["certificate"].Value)); bool isValid = certchain.Build(certificate); if (!isValid) { Console.WriteLine("Unable to validate certificate chain, failing signature check."); return(false); } // Get the contents nice and ready to be checked. string contents = data["body"].ToString(); byte[] contentbytes = new byte[contents.Length * sizeof(char)]; System.Buffer.BlockCopy(contents.ToCharArray(), 0, contentbytes, 0, contentbytes.Length); // Get the signature decoded. byte[] signaturebytes = Convert.FromBase64String(data["signatureinfo"]["signature"].Value); var rsa = certificate.PublicKey.Key as RSACryptoServiceProvider; // Check the signature! bool verified = rsa.VerifyData(contentbytes, "SHA256", signaturebytes); if (!verified) { Console.WriteLine("Signature check failed to match signature to signed data, failing signature check."); return(false); } // Well, it all checks out. return(true); } catch (Exception e) { Console.WriteLine("Unexpected exception in signature check, failing signature check."); Console.WriteLine("Exception was " + e.ToString()); return(false); } }
/// <summary> /// Load the specifications of all installed mods. /// </summary> /// <returns>A List of ModSpecifications, one per installed mod.</returns> List <ModSpecification> LoadModSpecifications() { // Mod specifications are kv files stored in /moddota/mods/ // the specifications just list filename keys, containing a CRC kv each (and maybe a path?) // there's also a single "version" key, which lists the version of the mod (for comparison with md) string folderpath = dotapath + "/moddota/mods/"; if (!Directory.Exists(folderpath)) { Directory.CreateDirectory(folderpath); } IEnumerable <string> mods = Directory.EnumerateFiles(folderpath, "*.mod"); List <ModSpecification> modspecs = new List <ModSpecification>(); foreach (string modfilename in mods) { Console.WriteLine("Parsing data for mod at " + modfilename); KV.KeyValue thisone = null; try { thisone = KV.KVParser.ParseKeyValueFile(modfilename); } catch (KV.KVParser.KeyValueParsingException) { Console.WriteLine("Error while KV parsing " + modfilename); } if (thisone == null) { // should queue-up re-download? continue; } ModSpecification ms = new ModSpecification(thisone); ms.name = thisone.Key; ms.version = new Version(); ms.files = new List <ModResource>(); foreach (KV.KeyValue k in thisone.Children) { if (k.Key == "version") { ms.version = new Version(k.GetString()); } if (k.Key == "resource") { ms.files.Add(new ModResource(k)); } } } return(modspecs); }
/// <summary> /// Construct a ModSpecification from the base data. /// </summary> /// <param name="sourcedata">The KeyValue from which this ModSpecification is to be constructed.</param> public ModSpecification(KV.KeyValue sourcedata) { if (!ModDotaHelper.modman.CCV.CheckSignature(sourcedata)) { throw new CryptoChainValidator.SignatureException(); } foreach (KV.KeyValue kv in sourcedata["body"].Children) { switch (kv.Key) { case "modinfo": break; default: files.Add(new ModResource(kv)); break; } } }
/// <summary> /// Construct a ModDirectory from the stored KV info /// </summary> /// <param name="des">The KV object containing the keyvalue structure as well as the signature for the moddirectory.</param> /// <param name="host">The host from which the ModDirectory was fetched, used for validation.</param> /// <exception cref="ModDotaHelper.ModDirectory.ModDirectorySignatureException">If the directory's signature is bad.</exception> public ModDirectory(KV.KeyValue des, string host) { bool passedvalidation = ModDotaHelper.modman.CCV.CheckSignature(des); if (!passedvalidation) { throw new CryptoChainValidator.SignatureException(); } foreach (KV.KeyValue kv in des["body"].Children) { switch (kv.Key) { case "version": version = new Version(kv.GetString()); break; default: entries.Add(new ModDirectoryEntry(kv)); break; } } }
/// <summary> /// Construct from a kv node, used when parsing .mod files. /// </summary> /// <param name="k"></param> public ModResource(KV.KeyValue k) { foreach (KV.KeyValue v in k.Children) { switch (v.Key) { case "CRC": CRC = UInt32.Parse(v.GetString()); break; case "internalpath": internalpath = v.GetString(); break; case "downloadurl": downloadurl = v.GetString(); break; case "signature": signature = v.GetString(); break; } } }
/// <summary> /// Get the mod list from the local cache. /// </summary> /// <param name="nodownload">If true, don't download new versions for missing/incorrect/outdated directories.</param> /// <param name="forcereacquire">If true, force a new download of all directories.</param> public ModDirectory GetModDirectories(bool nodownload = false, bool forcereacquire = false) { // First get the list of directories we need to look at if (!File.Exists(dotapath + "/moddota/directories.kv")) { return(null); } string contents = null; try { contents = File.ReadAllText(dotapath + "/moddota/directories.kv"); } catch (Exception) { Console.WriteLine("Couldn't access directories file!"); } KV.KeyValue kv = null; try { kv = KV.KVParser.ParseKeyValue(contents); } catch (KV.KVParser.KeyValueParsingException) { Console.WriteLine("poorly formatted kv file for directories, using default"); kv = new KV.KeyValue("directories"); KV.KeyValue defaultentry = new KV.KeyValue("moddota"); KV.KeyValue defaultdirectory = new KV.KeyValue("directory"); defaultdirectory.Set("https://moddota.com/mdc/directory.kv"); defaultentry.AddChild(defaultdirectory); KV.KeyValue defaultversion = new KV.KeyValue("version"); defaultversion.Set("https://moddota.com/mdc/directory.version"); defaultentry.AddChild(defaultversion); kv.AddChild(defaultentry); } ModDirectoryList mdl = new ModDirectoryList(kv); ModDirectory basemd = new ModDirectory(); foreach (Tuple <string, Uri, Uri> tpl in mdl.directories) { try { string directoryname = dotapath + "/moddota/dirs/" + tpl.Item1 + ".dir"; bool trieddownload = false; // If we don't have that particular directory, force a re-acquire if (!File.Exists(directoryname) || forcereacquire) { if (nodownload) { // don't have it and can't get it, just go to the next one. continue; } else { TryDownloadModDirectory(tpl.Item1, tpl.Item2); trieddownload = true; } } ModDirectory md = null; // Try to parse the file, since we have one try { md = new ModDirectory(KV.KVParser.ParseKeyValueFile(directoryname), tpl.Item2.Host); } catch (CryptoChainValidator.SignatureException) { // The signature was wrong. if (nodownload) { // Since we can't download a new version, we might as well just give up on this one continue; } else { md = TryDownloadModDirectory(tpl.Item1, tpl.Item2); trieddownload = true; } } catch (KV.KVParser.KeyValueParsingException) { // The format was wrong. if (nodownload) { // Since we can't download a new version, we might as well just give up on this one continue; } else { md = TryDownloadModDirectory(tpl.Item1, tpl.Item2); trieddownload = true; } } // Version check - don't check the version if we aren't going to download a new one anyway, and don't check it if we already just downloaded it. if (!nodownload && !trieddownload) { Version remoteversion; try { using (WebClient client = new WebClient()) { remoteversion = new Version(client.DownloadString(tpl.Item3)); } if (remoteversion > md.version) { if (nodownload) { // don't actually skip this time - we just let people play offline with the old version. } else { ModDirectory newmd = TryDownloadModDirectory(tpl.Item1, tpl.Item2); if (newmd != null) { md = newmd; } trieddownload = true; } } } catch (Exception) { Console.WriteLine("Encountered exception while parsing files for " + tpl.Item1 + "'s directory version information"); } } basemd.add(md); } catch (Exception) { Console.WriteLine("Failed to acquire or parse the ModDirectory from " + tpl.Item2.ToString()); } } return(basemd); }
/// <summary> /// Get a gameinfo with our override added. /// </summary> /// <returns>A KV structure describing the gameinfo.</returns> private static KV.KeyValue GetFixedGameInfo() { // This seemed like a good way to do it at the time... remind me to // add a more syntactically-compact way of doing this or just use a // string representation instead - this is 3x longer and 3000x more // complex/unreadable/error-prone. KV.KeyValue gameinfo = new KV.KeyValue("GameInfo"); KV.KeyValue game = new KV.KeyValue("game"); game.Set("DOTA 2"); gameinfo.AddChild(game); KV.KeyValue gamelogo = new KV.KeyValue("gamelogo"); gamelogo.Set(1); gameinfo.AddChild(gamelogo); KV.KeyValue type = new KV.KeyValue("type"); type.Set("multiplayer_only"); gameinfo.AddChild(type); KV.KeyValue nomodels = new KV.KeyValue("nomodels"); nomodels.Set(1); gameinfo.AddChild(nomodels); KV.KeyValue nohimodel = new KV.KeyValue("nohimodel"); nohimodel.Set(1); gameinfo.AddChild(nohimodel); KV.KeyValue nocrosshair = new KV.KeyValue("nocrosshair"); nocrosshair.Set(0); gameinfo.AddChild(nocrosshair); KV.KeyValue gamedata = new KV.KeyValue("GameData"); gamedata.Set("dota.fgd"); gameinfo.AddChild(gamedata); KV.KeyValue supportsdx8 = new KV.KeyValue("SupportsDX8"); supportsdx8.Set(0); gameinfo.AddChild(supportsdx8); KV.KeyValue filesystem = new KV.KeyValue("FileSystem"); KV.KeyValue SteamAppId = new KV.KeyValue("SteamAppId"); SteamAppId.Set(816); filesystem.AddChild(SteamAppId); KV.KeyValue ToolsAppId = new KV.KeyValue("ToolsAppId"); ToolsAppId.Set(211); filesystem.AddChild(ToolsAppId); KV.KeyValue SearchPaths = new KV.KeyValue("SearchPaths"); KV.KeyValue game0 = new KV.KeyValue("Game"); game0.Set("moddota"); SearchPaths.AddChild(game0); KV.KeyValue game1 = new KV.KeyValue("Game"); game1.Set("|gameinfo_path|."); SearchPaths.AddChild(game1); KV.KeyValue game2 = new KV.KeyValue("Game"); game2.Set("platform"); SearchPaths.AddChild(game2); filesystem.AddChild(SearchPaths); gameinfo.AddChild(filesystem); KV.KeyValue ToolsEnvironment = new KV.KeyValue("ToolsEnvironment"); KV.KeyValue Engine = new KV.KeyValue("Engine"); Engine.Set("Souce"); ToolsEnvironment.AddChild(Engine); KV.KeyValue UseVPlatform = new KV.KeyValue("UseVPlatform"); UseVPlatform.Set(1); ToolsEnvironment.AddChild(UseVPlatform); KV.KeyValue PythonVersion = new KV.KeyValue("PythonVersion"); PythonVersion.Set("2.7"); ToolsEnvironment.AddChild(PythonVersion); KV.KeyValue PythonHomeDisable = new KV.KeyValue("PythonHomeDisable"); PythonVersion.Set(1); ToolsEnvironment.AddChild(PythonVersion); gameinfo.AddChild(ToolsEnvironment); return(gameinfo); }
/// <summary> /// Grab all of the keyvalues from a string. /// </summary> /// <param name="contents">The string containing keyvalues</param> /// <param name="allowunnamedkeys">Whether or not to allow unnamed blocks (used in bsp entity lump)</param> /// <returns>An array containing all root-level KeyValues in the string</returns> /// <exception cref="ModDotaHelper.KV.KVParser.KeyValueParsingException">Throws one of these if parsing fails</exception> public static KeyValue[] ParseAllKeyValues(string contents, bool allowunnamedkeys = false) { if (contents == null) { throw new KeyValueParsingException("Contents string was null!", new ArgumentNullException()); } try { parseEnum parseState = parseEnum.lookingForKey; KeyValue basekv = new KeyValue("base"); // file contents are interpreted as children of this keyvalue KeyValue curparent = basekv; for (int i = 0; i < contents.Length; i++) { // go until next symbol if (contents[i] == ' ' || contents[i] == '\t' || contents[i] == '\n' || contents[i] == '\r') { continue; } switch (parseState) { case parseEnum.lookingForKey: if (contents[i] == '{') { if (!allowunnamedkeys) { throw new KeyValueParsingException("Hit unnamed key while parsing without unnamed keys enabled.", null); } // This is a special case - some kv files, in particular bsp entity lumps, have unkeyed kvs KeyValue cur = new KeyValue("UNNAMED"); curparent.AddChild(cur); curparent = cur; parseState = parseEnum.lookingForValue; } else if (contents[i] == '"' || contents[i] == '\'') { //quoted key int j = i + 1; if (j >= contents.Length) { throw new KeyValueParsingException("Couldn't find terminating '" + contents[i].ToString() + "' for key started at position " + i.ToString(), null); } while (contents[j] != contents[i]) { // handle escaped quotes if (contents[j] == '\\') { j++; } j++; if (j >= contents.Length) { throw new KeyValueParsingException("Couldn't find terminating '" + contents[i].ToString() + "' for key started at position " + i.ToString(), null); } } //ok, now contents[i] and contents[j] are the same character, on either end of the key KeyValue cur = new KeyValue(contents.Substring(i + 1, j - (i + 1))); curparent.AddChild(cur); curparent = cur; parseState = parseEnum.lookingForValue; i = j; } else if (Char.IsLetter(contents[i])) { //un-quoted key int j = i; while (contents[j] != ' ' && contents[j] != '\t' && contents[j] != '\n' && contents[j] != '\r') { j++; if (j > contents.Length) { throw new KeyValueParsingException("Couldn't find end of key started at position " + i.ToString(), null); } } KeyValue cur = new KeyValue(contents.Substring(i, j - i)); curparent.AddChild(cur); curparent = cur; parseState = parseEnum.lookingForValue; i = j; } else if (contents[i] == '}') { //drop one level curparent = curparent.Parent; } else if (contents[i] == '/') { if (i + 1 < contents.Length && contents[i + 1] == '/') { // we're in a comment! throw stuff away until the next \n while (i < contents.Length && contents[i] != '\n') { i++; } } } else { throw new KeyValueParsingException("Unexpected '" + contents[i].ToString() + "' at position " + i.ToString(), null); } break; case parseEnum.lookingForValue: if (contents[i] == '{') { // it's a list of children // thankfully, we don't actually need to handle this! parseState = parseEnum.lookingForKey; } else if (contents[i] == '"' || contents[i] == '\'') { //quoted value int j = i + 1; while (contents[j] != contents[i]) { // handle escaped quotes if (contents[j] == '\\') { j++; } j++; if (j > contents.Length) { throw new KeyValueParsingException("Couldn't find terminating '" + contents[i].ToString() + "' for key started at position " + i.ToString(), null); } } //ok, now contents[i] and contents[j] are the same character, on either end of the value curparent.Set(contents.Substring(i + 1, j - (i + 1))); curparent = curparent.Parent; parseState = parseEnum.lookingForKey; i = j; } else if (contents[i] == '/') { if (i + 1 < contents.Length && contents[i + 1] == '/') { // we're in a comment! throw stuff away until the next \n while (i < contents.Length && contents[i] != '\n') { i++; } } } else if (!Char.IsWhiteSpace(contents[i])) { int j = i; while (contents[j] != ' ' && contents[j] != '\t' && contents[j] != '\n' && contents[j] != '\r') { j++; if (j > contents.Length) { // a value ending the file counts as ending the value break; } } curparent.Set(contents.Substring(i, j - i)); curparent = curparent.Parent; parseState = parseEnum.lookingForKey; i = j; } else { throw new KeyValueParsingException("Unexpected '" + contents[i].ToString() + "' at position " + i.ToString(), null); } break; } } // At the end of the file, we should be looking for another key if (parseState != parseEnum.lookingForKey) { throw new KeyNotFoundException("File ended while looking for value", null); } // At the end of the file, all block values should be closed if (curparent != basekv) { throw new KeyNotFoundException("Unterminated child blocks", null); } KeyValue[] ret = basekv.Children.ToArray <KeyValue>(); basekv.clearChildParents(); return(ret); } catch (KeyValueParsingException e) { throw e; } catch (Exception e) { throw new KeyValueParsingException("Hit an exception while parsing kv data!", e); } }
/// <summary> /// Read the configuration file. May need to be made a bit more fail- /// safe, there's a few potential exceptions not handled. /// </summary> public static void ReadConfig() { string configpath = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData) + "/config.txt"; if (!File.Exists(configpath)) { // Uh oh! We don't have a config file! // Quick, before they realize, generate a new config file! // By doing this approach, we can get the elevated privs needed to peek at the registry for dota' location. ProcessStartInfo ps = new ProcessStartInfo(); ps.Verb = "runas"; ps.FileName = "GenerateBaseConfiguration.exe"; try { Process generator = Process.Start(ps); generator.WaitForExit(); } catch (Win32Exception) { //can't really do anything about it here } //ok, we've writen it, it's all good } // The file may still not exist if the generator failed if (File.Exists(configpath)) { string contents; try { using (StreamReader readfrom = new StreamReader(configpath)) { contents = readfrom.ReadToEnd(); } } catch (Exception) { // There's a lot of reasons why it might fail, so just handle it with defaults for now Console.WriteLine("Failed to read config file, using default values..."); goto parsefailed; } KV.KeyValue confignode = null; KV.KeyValue[] confignodes; try { confignodes = KV.KVParser.ParseAllKeyValues(contents); } catch (KV.KVParser.KeyValueParsingException) { Console.WriteLine("Failed to parse config file, using default values..."); goto parsefailed; } foreach (KV.KeyValue kv in confignodes) { if (kv.Key == "config") { confignode = kv; } } if (confignode == null) { Console.WriteLine("Couldn't find config node in configuration, using default values..."); goto parsefailed; } foreach (KV.KeyValue child in confignode.Children) { switch (child.Key) { case "dotaDir": DotaPath = child.GetString(); break; default: // We haven't defined anything else yet. continue; } } // Check that required values are set if (DotaPath == null) { Console.WriteLine("Couldn't find required values in config file, using default values..."); goto parsefailed; } } else { goto parsefailed; } return; // PARSE FAILURE HANDLING parsefailed: // can't read the configuration, and can't generate a new one. Oh well, use defaults. // Default dota install dir DotaPath = "C:/Program Files (x86)/Steam/steamapps/common/dota 2 beta"; return; }