/* * InspIRCd -- Internet Relay Chat Daemon * * Copyright (C) 2018 linuxdaemon * Copyright (C) 2013-2014, 2016-2025 Sadie Powell * Copyright (C) 2012-2014 Attila Molnar * Copyright (C) 2012 Robby * Copyright (C) 2009-2010 Daniel De Graaf * * This file is part of InspIRCd. InspIRCd is free software: you can * redistribute it and/or modify it under the terms of the GNU General Public * License as published by the Free Software Foundation, version 2. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include #include "inspircd.h" #include "configparser.h" #include "timeutils.h" #include "utility/string.h" #ifdef _WIN32 # define pclose _pclose # define popen _popen #else # include #endif namespace { std::string GetPrintable(int chr) { if (isprint(chr)) return FMT::format("{} (0x{:x})", (char)chr, chr); else return FMT::format("0x{:x}", chr); } } enum ParseFlags { // Executable includes are disabled. FLAG_NO_EXEC = 2, // All includes are disabled. FLAG_NO_INC = 4, // &env.FOO; is disabled. FLAG_NO_ENV = 8, // It's okay if an include doesn't exist. FLAG_MISSING_OKAY = 16 }; struct Parser final { ParseStack& stack; int flags; FilePtr file; FilePosition current; FilePosition last_tag; std::shared_ptr tag; int ungot = -1; std::string mandatory_tag; Parser(ParseStack& me, int myflags, FilePtr conf, const std::string& name, const std::string& mandatorytag) : stack(me) , flags(myflags) , file(std::move(conf)) , current(name, 1, 0) , last_tag(name, 0, 0) , mandatory_tag(mandatorytag) { } int next(bool eof_ok = false) { if (ungot != -1) { int ch = ungot; ungot = -1; return ch; } int ch = fgetc(file.get()); if (ch == EOF && !eof_ok) { throw CoreException("Unexpected end-of-file"); } else if (ch == '\n') { current.line++; current.column = 1; } else { current.column++; } return ch; } void unget(int ch) { if (ungot != -1) throw CoreException("INTERNAL ERROR: cannot unget twice"); ungot = ch; } void comment() { while (true) { int ch = next(true); if (ch == '\n') return; if (ch == EOF) { unget(ch); return; } } } static bool wordchar(int ch) { return isalnum(ch) || ch == '-' || ch == '.' || ch == '_'; } void nextword(std::string& rv) { int ch = next(); while (isspace(ch)) ch = next(); while (wordchar(ch)) { rv.push_back(ch); ch = next(); } unget(ch); } bool kv() { std::string key; nextword(key); int ch = next(); if (ch == '>' && key.empty()) { return false; } else if (ch == '#' && key.empty()) { comment(); return true; } else if (ch != '=') { throw CoreException("Invalid character in key " + key + ": " + GetPrintable(ch)); } std::string value; ch = next(); if (ch != '"') { throw CoreException("Invalid character in value of <" + tag->name + ":" + key + ">: " + GetPrintable(ch)); } while (true) { ch = next(); if (ch == '&') { std::string varname; while (true) { ch = next(); if (wordchar(ch) || (varname.empty() && ch == '#')) varname.push_back(ch); else if (ch == ';') break; else { stack.errors.push_back(FMT::format("Invalid XML entity name in value of <{}:{}>", tag->name, key)); stack.errors.push_back("To include an ampersand or quote, use & or ""); throw CoreException("Parse error"); } } if (varname.empty()) throw CoreException("Empty XML entity reference"); else if (varname[0] == '#' && (varname.size() == 1 || (varname.size() == 2 && varname[1] == 'x'))) throw CoreException("Empty numeric character reference"); else if (varname[0] == '#') { const char* cvarname = varname.c_str(); char* endptr; unsigned long lvalue; if (cvarname[1] == 'x') lvalue = strtoul(cvarname + 2, &endptr, 16); else lvalue = strtoul(cvarname + 1, &endptr, 10); if (*endptr != '\0' || lvalue > 255) throw CoreException("Invalid numeric character reference '&" + varname + ";'"); value.push_back(static_cast(lvalue)); } else if (varname.compare(0, 4, "env.") == 0) { if (flags & FLAG_NO_ENV) throw CoreException("XML environment entity reference in file included with noenv=\"yes\""); const char* envstr = getenv(varname.c_str() + 4); if (!envstr) throw CoreException("Undefined XML environment entity reference '&" + varname + ";'"); value.append(envstr); } else { insp::flat_map::iterator var = stack.vars.find(varname); if (var == stack.vars.end()) throw CoreException("Undefined XML entity reference '&" + varname + ";'"); value.append(var->second); } } else if (ch == '"') break; else if (ch != '\r') value.push_back(ch); } if (!tag->GetItems().insert({key, value}).second) throw CoreException("Duplicate key '" + key + "' found"); return true; } void dotag() { last_tag = current; std::string name; nextword(name); int spc = next(); if (spc == '>') unget(spc); else if (!isspace(spc)) { throw CoreException("Invalid character in tag name: " + GetPrintable(spc)); } if (name.empty()) throw CoreException("Empty tag name"); tag = std::make_shared(name, last_tag); while (kv()) { // Do nothing here (silences a GCC warning). } if (name == mandatory_tag) { // Found the mandatory tag mandatory_tag.clear(); } if (insp::equalsci(name, "include")) { stack.DoInclude(tag, flags); } else if (insp::equalsci(name, "files")) { for (const auto& [key, value] : tag->GetItems()) stack.DoReadFile(key, value, flags, false); } else if (insp::equalsci(name, "execfiles")) { for (const auto& [key, value] : tag->GetItems()) stack.DoReadFile(key, value, flags, true); } else if (insp::equalsci(name, "define")) { const std::string varname = tag->getString("name"); if (varname.empty()) throw CoreException("Variable definition must include a variable name, at " + tag->source.str()); if (stack.vars.find(varname) == stack.vars.end() || tag->getBool("replace", true)) stack.vars[varname] = tag->getString("value"); } else { stack.output.emplace(name, tag); } // this is not a leak; shared_ptr takes care of the delete tag = nullptr; } bool outer_parse() { try { while (true) { int ch = next(true); switch (ch) { case EOF: // this is the one place where an EOF is not an error if (!mandatory_tag.empty()) throw CoreException("Mandatory tag \"" + mandatory_tag + "\" not found"); return true; case '#': comment(); break; case '<': dotag(); break; case ' ': case '\r': case '\t': case '\n': break; case 0xFE: case 0xFF: stack.errors.push_back("Do not save your files as UTF-16 or UTF-32, use UTF-8!"); [[fallthrough]]; default: throw CoreException("Syntax error - start of tag expected"); } } } catch (const CoreException& err) { stack.errors.push_back(FMT::format("{} at {}", err.GetReason(), current.str())); if (tag) stack.errors.back().append(FMT::format(" (inside {} tag on line {})", tag->name, tag->source.line)); else if (last_tag.line) stack.errors.back().append(FMT::format(" (last tag was on line {})", tag->source.line)); } return false; } }; FilePosition::FilePosition(const std::string& Name, unsigned long Line, unsigned long Column) : name(Name) , line(Line) , column(Column) { } std::string FilePosition::str() const { return FMT::format("{}:{}:{}", name, line, column); } void ParseStack::DoInclude(const std::shared_ptr& tag, int flags) { if (flags & FLAG_NO_INC) throw CoreException("Invalid tag in file included with noinclude=\"yes\""); std::string mandatorytag; tag->readString("mandatorytag", mandatorytag); std::string name; if (tag->readString("file", name)) { if (tag->getBool("noinclude", false)) flags |= FLAG_NO_INC; if (tag->getBool("noexec", false)) flags |= FLAG_NO_EXEC; if (tag->getBool("noenv", false)) flags |= FLAG_NO_ENV; if (tag->getBool("missingokay", false)) flags |= FLAG_MISSING_OKAY; else flags &= ~FLAG_MISSING_OKAY; if (!ParseFile(name, flags, mandatorytag)) throw CoreException("Included"); } else if (tag->readString("directory", name)) { if (tag->getBool("noinclude", false)) flags |= FLAG_NO_INC; if (tag->getBool("noexec", false)) flags |= FLAG_NO_EXEC; if (tag->getBool("noenv", false)) flags |= FLAG_NO_ENV; const std::string includedir = ServerInstance->Config->Paths.PrependConfig(name); std::set configs; try { for (const auto& entry : std::filesystem::directory_iterator(includedir)) { if (!entry.is_regular_file()) continue; const std::string path = entry.path().string(); if (InspIRCd::Match(path, "*.conf")) configs.insert(path); } } catch (const std::filesystem::filesystem_error& err) { throw CoreException("Unable to read directory for include " + includedir + ": " + err.what()); } for (const auto& config : configs) { if (!ParseFile(config, flags, mandatorytag)) throw CoreException("Included"); } } else if (tag->readString("executable", name)) { if (flags & FLAG_NO_EXEC) throw CoreException("Invalid tag in file included with noexec=\"yes\""); if (tag->getBool("noinclude", false)) flags |= FLAG_NO_INC; if (tag->getBool("noexec", true)) flags |= FLAG_NO_EXEC; if (tag->getBool("noenv", true)) flags |= FLAG_NO_ENV; if (!ParseFile(name, flags, mandatorytag, true)) throw CoreException("Included"); } } FilePtr ParseStack::DoOpenFile(const std::string& name, bool isexec) { if (isexec) { ServerInstance->Logs.Debug("CONFIG", "Opening executable: {}", name); return FilePtr(popen(name.c_str(), "r"), pclose); } const std::string path = ServerInstance->Config->Paths.PrependConfig(name); ServerInstance->Logs.Debug("CONFIG", "Opening file: {}", path); #ifndef _WIN32 struct stat pathinfo; if (stat(path.c_str(), &pathinfo) == 0) { if (getegid() != pathinfo.st_gid) { ServerInstance->Logs.Warning("CONFIG", "Possible configuration error: {} is owned by group {} but the server is running as group {}.", path, pathinfo.st_gid, getegid()); } if (geteuid() != pathinfo.st_uid) { ServerInstance->Logs.Warning("CONFIG", "Possible configuration error: {} is owned by user {} but the server is running as user {}.", path, pathinfo.st_uid, geteuid()); } } #endif return FilePtr(fopen(path.c_str(), "r"), fclose); } void ParseStack::DoReadFile(const std::string& key, const std::string& name, int flags, bool exec) { if (flags & FLAG_NO_INC) throw CoreException("Invalid tag in file included with noinclude=\"yes\""); if (exec && (flags & FLAG_NO_EXEC)) throw CoreException("Invalid tag in file included with noexec=\"yes\""); if (FilesOutput.emplace(key, std::make_pair(name, exec)).second) { ServerInstance->Logs.Debug("CONFIG", "Stored config key: {} => {} (executable: {}).", key, name, exec ? "yes" : "no"); } } ParseStack::ParseStack(ServerConfig* conf) : output(conf->config_data) , FilesOutput(conf->filesources) , errors(conf->errors) { vars = { // Special character escapes. { "newline", "\n" }, { "nl", "\n" }, // XML escapes. { "amp", "&" }, { "apos", "'" }, { "gt", ">" }, { "lt", "<" }, { "quot", "\"" }, // Directories that were set at build time. { "dir.config", INSPIRCD_CONFIG_PATH }, { "dir.example", INSPIRCD_EXAMPLE_PATH }, { "dir.data", INSPIRCD_DATA_PATH }, { "dir.log", INSPIRCD_LOG_PATH }, { "dir.module", INSPIRCD_MODULE_PATH }, { "dir.runtime", INSPIRCD_RUNTIME_PATH }, // IRC formatting codes. { "irc.bold", "\x02" }, { "irc.color", "\x03" }, { "irc.colour", "\x03" }, { "irc.hexcolor", "\x04" }, { "irc.hexcolour", "\x04" }, { "irc.italic", "\x1D" }, { "irc.monospace", "\x11" }, { "irc.reset", "\x0F" }, { "irc.reverse", "\x16" }, { "irc.strikethrough", "\x1E" }, { "irc.underline", "\x1F" }, }; } bool ParseStack::ParseFile(const std::string& path, int flags, const std::string& mandatory_tag, bool isexec) { if (stdalgo::isin(reading, path)) throw CoreException((isexec ? "Executable " : "File ") + path + " is included recursively (looped inclusion)"); /* It's not already included, add it to the list of files we've loaded */ FilePtr file = DoOpenFile(path, isexec); if (!file) { if (flags & FLAG_MISSING_OKAY) return true; throw CoreException(FMT::format("Could not read \"{}\" for include: {}", path, strerror(errno))); } reading.push_back(path); Parser p(*this, flags, std::move(file), path, mandatory_tag); bool ok = p.outer_parse(); reading.pop_back(); return ok; } void ConfigTag::LogMalformed(const std::string& key, const std::string& val, const std::string& def, const std::string& reason) const { ServerInstance->Logs.Warning("CONFIG", "The value of <{}:{}> at {} ({}) is {}; using the default ({}) instead.", name, key, source.str(), val, reason, def); } bool ConfigTag::readString(const std::string& key, std::string& value, bool allow_lf) const { for (const auto& [ikey, ivalue] : items) { if (!insp::equalsci(ikey, key)) continue; value = ivalue; if (!allow_lf && (value.find('\n') != std::string::npos)) { ServerInstance->Logs.Warning("CONFIG", "Value of <" + name + ":" + key + "> at " + source.str() + " contains a linefeed, and linefeeds in this value are not permitted -- stripped to spaces."); for (auto& chr : value) { if (chr == '\n') chr = ' '; } } return true; } return false; } std::string ConfigTag::getString(const std::string& key, const std::string& def, const std::function& validator) const { std::string res; if (!readString(key, res)) return def; if (!validator(res)) { LogMalformed(key, res, def, "not valid"); return def; } return res; } std::string ConfigTag::getString(const std::string& key, const std::string& def, size_t minlen, size_t maxlen) const { std::string res; if (!readString(key, res)) return def; if (res.length() < minlen || res.length() > maxlen) { LogMalformed(key, res, def, FMT::format("not between {} and {} characters in length", minlen, maxlen)); return def; } return res; } namespace { /** Check for an invalid magnitude specifier. If one is found a warning is logged and the * value is corrected (set to \p def). * @param tag The tag name; used in the warning message. * @param key The key name; used in the warning message. * @param val The full value set in the config as a string. * @param num The value to verify and modify if needed. * @param def The default value, \p res will be set to this if \p tail does not contain a. * valid magnitude specifier. * @param tail The location in the config value at which the magnifier is located. */ template void CheckMagnitude(const ConfigTag* tag, const std::string& key, const std::string& val, Numeric& num, Numeric def, const char* tail) { // If this is NULL then no magnitude specifier was given. if (!*tail) return; switch (toupper(*tail)) { case 'K': num *= 1024; return; case 'M': num *= 1024 * 1024; return; case 'G': num *= 1024 * 1024 * 1024; return; default: num = def; tag->LogMalformed(key, val, ConvToStr(def), FMT::format("contains an invalid magnitude specifier ({})", *tail)); return; } } /** Check for an out of range value. If the value falls outside the boundaries a warning is * logged and the value is corrected (set to \p def). * @param tag The tag name; used in the warning message. * @param key The key name; used in the warning message. * @param num The value to verify and modify if needed. * @param def The default value, \p res will be set to this if (min <= res <= max) doesn't hold true. * @param min Minimum accepted value for \p res. * @param max Maximum accepted value for \p res. */ template void CheckRange(const ConfigTag* tag, const std::string& key, Numeric& num, Numeric def, Numeric min, Numeric max) { if (num >= min && num <= max) return; tag->LogMalformed(key, ConvToStr(num), ConvToStr(def), FMT::format("not between {} and {}", min, max)); num = def; } } intmax_t ConfigTag::getSInt(const std::string& key, intmax_t def, intmax_t min, intmax_t max) const { std::string result; if(!readString(key, result) || result.empty()) return def; const char* res_cstr = result.c_str(); char* res_tail = nullptr; intmax_t res = strtoimax(res_cstr, &res_tail, 0); if (res_tail == res_cstr) return def; CheckMagnitude(this, key, result, res, def, res_tail); CheckRange(this, key, res, def, min, max); return res; } uintmax_t ConfigTag::getUInt(const std::string& key, uintmax_t def, uintmax_t min, uintmax_t max) const { std::string result; if (!readString(key, result) || result.empty()) return def; const char* res_cstr = result.c_str(); char* res_tail = nullptr; uintmax_t res = strtoumax(res_cstr, &res_tail, 0); if (res_tail == res_cstr) return def; CheckMagnitude(this, key, result, res, def, res_tail); CheckRange(this, key, res, def, min, max); return res; } unsigned long ConfigTag::getDuration(const std::string& key, unsigned long def, unsigned long min, unsigned long max) const { std::string duration; if (!readString(key, duration) || duration.empty()) return def; unsigned long ret; if (!Duration::TryFrom(duration, ret)) { LogMalformed(key, duration, ConvToStr(def), "is not a duration"); return def; } CheckRange(this, key, ret, def, min, max); return ret; } long double ConfigTag::getFloat(const std::string& key, long double def, long double min, long double max) const { std::string result; if (!readString(key, result)) return def; long double res = strtold(result.c_str(), nullptr); CheckRange(this, key, res, def, min, max); return res; } bool ConfigTag::getBool(const std::string& key, bool def) const { std::string result; if(!readString(key, result) || result.empty()) return def; if (insp::equalsci(result, "yes") || insp::equalsci(result, "true") || insp::equalsci(result, "on")) return true; if (insp::equalsci(result, "no") || insp::equalsci(result, "false") || insp::equalsci(result, "off")) return false; LogMalformed(key, result, def ? "yes" : "no", "is not a boolean"); return def; } unsigned char ConfigTag::getCharacter(const std::string& key, unsigned char def, bool emptynul) const { std::string result; if (!readString(key, result)) return def; if (result.size() != 1) return result.empty() && emptynul ? 0 : def; return result[0]; } ConfigTag::ConfigTag(const std::string& Name, const FilePosition& Source) : name(Name) , source(Source) { }