250 lines
9.2 KiB
C++
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;
|
|
}
|