/*********************************/ /* */ /* o.riabenkyi@gmain.com */ /* */ /*********************************/ #include "config/ConfigurationParser.hpp" #include #include #include #include #include ConfigurationParser::ConfigurationParser() : _lines(), _pos(0) {} ConfigurationParser::~ConfigurationParser() {} std::string ConfigurationParser::readFile(const std::string &path) const { std::ifstream file(path.c_str()); if (!file.is_open()) throw std::runtime_error("unable to open configuration file '" + path + "'"); std::ostringstream buffer; buffer << file.rdbuf(); return buffer.str(); } std::string ConfigurationParser::stripComment(const std::string &line) { size_t hash = line.find('#'); if (hash == std::string::npos) return line; return line.substr(0, hash); } std::vector ConfigurationParser::splitTokens(const std::string &line) { std::vector tokens; std::istringstream stream(line); std::string token; while (stream >> token) tokens.push_back(token); return tokens; } std::string ConfigurationParser::joinTokens(const std::vector &tokens) { std::string result; for (size_t i = 0; i < tokens.size(); ++i) { if (i > 0) result += ' '; result += tokens[i]; } return result; } void ConfigurationParser::tokenizeLines(const std::string &content) { _lines.clear(); _pos = 0; std::istringstream stream(content); std::string rawLine; while (std::getline(stream, rawLine)) { std::vector tokens = splitTokens(stripComment(rawLine)); if (!tokens.empty()) _lines.push_back(tokens); } } bool ConfigurationParser::hasNext() const { return _pos < _lines.size(); } const std::vector &ConfigurationParser::peek() const { if (!hasNext()) throw std::runtime_error("unexpected end of configuration file"); return _lines[_pos]; } std::vector ConfigurationParser::next() { if (!hasNext()) throw std::runtime_error("unexpected end of configuration file"); return _lines[_pos++]; } int ConfigurationParser::toInt(const std::string &s) { if (s.empty()) throw std::runtime_error("expected a number but got an empty value"); for (size_t i = 0; i < s.size(); ++i) { if (!std::isdigit(static_cast(s[i]))) throw std::runtime_error("expected a number but got '" + s + "'"); } return std::atoi(s.c_str()); } size_t ConfigurationParser::parseSize(const std::string &s) { if (s.empty()) throw std::runtime_error("expected a size but got an empty value"); size_t end = s.size(); size_t multiplier = 1; char suffix = static_cast(std::toupper(static_cast(s[end - 1]))); if (suffix == 'K') { multiplier = 1024; --end; } else if (suffix == 'M') { multiplier = 1024 * 1024; --end; } else if (suffix == 'G') { multiplier = 1024 * 1024 * 1024; --end; } std::string digits = s.substr(0, end); if (digits.empty()) throw std::runtime_error("invalid size value '" + s + "'"); for (size_t i = 0; i < digits.size(); ++i) { if (!std::isdigit(static_cast(digits[i]))) throw std::runtime_error("invalid size value '" + s + "'"); } return static_cast(std::atol(digits.c_str())) * multiplier; } void ConfigurationParser::applyServerDirective(ServerConfig &server, const std::string &name, const std::vector &values) { if (name == "listen") { if (values.size() != 1) throw std::runtime_error("'listen' directive requires exactly one value (interface:port or port)"); size_t colon = values[0].find(':'); if (colon != std::string::npos) server.addListen(values[0].substr(0, colon), toInt(values[0].substr(colon + 1))); else server.addListen("0.0.0.0", toInt(values[0])); } else if (name == "server_name") { if (values.empty()) throw std::runtime_error("'server_name' directive requires at least one value"); for (std::vector::const_iterator it = values.begin(); it != values.end(); ++it) server.addServerName(*it); } else if (name == "root") { if (values.size() != 1) throw std::runtime_error("'root' directive requires exactly one value"); server.setRoot(values[0]); } else if (name == "client_max_body_size") { if (values.size() != 1) throw std::runtime_error("'client_max_body_size' directive requires exactly one value"); server.setClientMaxBodySize(parseSize(values[0])); } else if (name == "error_page") { if (values.size() < 2) throw std::runtime_error("'error_page' directive requires at least one code and a path"); const std::string &path = values.back(); for (size_t i = 0; i + 1 < values.size(); ++i) server.addErrorPage(toInt(values[i]), path); } else { throw std::runtime_error("unknown directive '" + name + "' in server block"); } } void ConfigurationParser::applyLocationDirective(LocationConfig &location, const std::string &name, const std::vector &values) { if (name == "root") { if (values.size() != 1) throw std::runtime_error("'root' directive requires exactly one value"); location.setRoot(values[0]); } else if (name == "methods") { if (values.empty()) throw std::runtime_error("'methods' directive requires at least one value"); location.setMethods(values); } else if (name == "autoindex") { if (values.size() != 1 || (values[0] != "on" && values[0] != "off")) throw std::runtime_error("'autoindex' directive expects 'on' or 'off'"); location.setAutoindex(values[0] == "on"); } else if (name == "index") { if (values.size() != 1) throw std::runtime_error("'index' directive requires exactly one value"); location.setIndex(values[0]); } else if (name == "return") { if (values.size() != 2) throw std::runtime_error("'return' directive requires a status code and a target"); location.setReturn(toInt(values[0]), values[1]); } else if (name == "upload_store") { if (values.size() != 1) throw std::runtime_error("'upload_store' directive requires exactly one value"); location.setUploadStore(values[0]); } else if (name == "cgi_extension") { if (values.size() != 1) throw std::runtime_error("'cgi_extension' directive requires exactly one value"); location.setCgiExtension(values[0]); } else if (name == "cgi_pass") { if (values.size() != 1) throw std::runtime_error("'cgi_pass' directive requires exactly one value"); location.setCgiPass(values[0]); } else { throw std::runtime_error("unknown directive '" + name + "' in location block"); } } LocationConfig ConfigurationParser::parseLocationBlock(const std::string &path) { LocationConfig location; location.setPath(path); while (hasNext()) { const std::vector &line = peek(); if (line.size() == 1 && line[0] == "}") { next(); return location; } std::vector tokens = next(); std::vector values(tokens.begin() + 1, tokens.end()); applyLocationDirective(location, tokens[0], values); } throw std::runtime_error("unterminated location block '" + path + "', missing closing '}'"); } ServerConfig ConfigurationParser::parseServerBlock() { ServerConfig server; while (hasNext()) { const std::vector &line = peek(); if (line.size() == 1 && line[0] == "}") { next(); return server; } if (line[0] == "location") { if (line.size() != 3 || line[2] != "{") throw std::runtime_error("invalid 'location' opening, expected 'location {' but got '" + joinTokens(line) + "'"); std::string path = line[1]; next(); server.addLocation(parseLocationBlock(path)); } else { std::vector tokens = next(); std::vector values(tokens.begin() + 1, tokens.end()); applyServerDirective(server, tokens[0], values); } } throw std::runtime_error("unterminated server block, missing closing '}'"); } Config ConfigurationParser::parse(const std::string &path) { tokenizeLines(readFile(path)); Config config; while (hasNext()) { const std::vector &line = peek(); if (line.size() != 2 || line[0] != "server" || line[1] != "{") throw std::runtime_error("expected 'server {' block but got '" + joinTokens(line) + "'"); next(); config.addServer(parseServerBlock()); } config.validate(); return config; }