Files
codam-webserver-team/src/config/ConfigurationParser.cpp
T

250 lines
9.2 KiB
C++

/*********************************/
/* */
/* o.riabenkyi@gmain.com */
/* */
/*********************************/
#include "config/ConfigurationParser.hpp"
#include <cctype>
#include <cstdlib>
#include <fstream>
#include <sstream>
#include <stdexcept>
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<std::string> ConfigurationParser::splitTokens(const std::string &line) {
std::vector<std::string> tokens;
std::istringstream stream(line);
std::string token;
while (stream >> token)
tokens.push_back(token);
return tokens;
}
std::string ConfigurationParser::joinTokens(const std::vector<std::string> &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<std::string> tokens = splitTokens(stripComment(rawLine));
if (!tokens.empty())
_lines.push_back(tokens);
}
}
bool ConfigurationParser::hasNext() const { return _pos < _lines.size(); }
const std::vector<std::string> &ConfigurationParser::peek() const {
if (!hasNext())
throw std::runtime_error("unexpected end of configuration file");
return _lines[_pos];
}
std::vector<std::string> 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<unsigned char>(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<char>(std::toupper(static_cast<unsigned char>(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<unsigned char>(digits[i])))
throw std::runtime_error("invalid size value '" + s + "'");
}
return static_cast<size_t>(std::atol(digits.c_str())) * multiplier;
}
void ConfigurationParser::applyServerDirective(ServerConfig &server, const std::string &name,
const std::vector<std::string> &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<std::string>::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<std::string> &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<std::string> &line = peek();
if (line.size() == 1 && line[0] == "}") {
next();
return location;
}
std::vector<std::string> tokens = next();
std::vector<std::string> 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<std::string> &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 <path> {' but got '" +
joinTokens(line) + "'");
std::string path = line[1];
next();
server.addLocation(parseLocationBlock(path));
} else {
std::vector<std::string> tokens = next();
std::vector<std::string> 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<std::string> &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;
}