first approximation of the parser
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
/*********************************/
|
||||
/* */
|
||||
/* o.riabenkyi@gmain.com */
|
||||
/* */
|
||||
/*********************************/
|
||||
|
||||
#include "config/Config.hpp"
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
namespace {
|
||||
|
||||
// "0.0.0.0" binds every interface, so a listen on it can never share a
|
||||
// socket with a listen on a specific address on the same port: the two are
|
||||
// separate bind() calls and the second one always fails with EADDRINUSE.
|
||||
bool isWildcardOverlap(const std::string &a, const std::string &b) {
|
||||
return a != b && (a == "0.0.0.0" || b == "0.0.0.0");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Config::Config() : _servers() {}
|
||||
|
||||
void Config::addServer(const ServerConfig &server) { _servers.push_back(server); }
|
||||
|
||||
const std::vector<ServerConfig> &Config::getServers() const { return _servers; }
|
||||
|
||||
void Config::validate() const {
|
||||
if (_servers.empty())
|
||||
throw std::runtime_error("configuration must define at least one server block");
|
||||
|
||||
for (std::vector<ServerConfig>::const_iterator it = _servers.begin(); it != _servers.end(); ++it)
|
||||
it->validate();
|
||||
|
||||
// Two server blocks may not listen on the same host:port while also
|
||||
// sharing a server_name (or both leaving server_name empty), since that
|
||||
// combination would be ambiguous to route.
|
||||
for (size_t i = 0; i < _servers.size(); ++i) {
|
||||
for (size_t j = i + 1; j < _servers.size(); ++j) {
|
||||
const ServerConfig &a = _servers[i];
|
||||
const ServerConfig &b = _servers[j];
|
||||
|
||||
for (std::vector<ServerConfig::Listen>::const_iterator la = a.getListens().begin();
|
||||
la != a.getListens().end(); ++la) {
|
||||
for (std::vector<ServerConfig::Listen>::const_iterator lb = b.getListens().begin();
|
||||
lb != b.getListens().end(); ++lb) {
|
||||
if (la->port != lb->port)
|
||||
continue;
|
||||
|
||||
// A wildcard listen can never coexist with a specific-address
|
||||
// listen on the same port, no matter the server_names: they are
|
||||
// two separate sockets and the second bind() always fails.
|
||||
if (isWildcardOverlap(la->host, lb->host))
|
||||
throw std::runtime_error("listen " + la->host + ":" + std::to_string(la->port) +
|
||||
" conflicts with " + lb->host + ":" +
|
||||
std::to_string(lb->port) +
|
||||
" (a wildcard address binds every interface)");
|
||||
|
||||
if (la->host != lb->host)
|
||||
continue;
|
||||
|
||||
if (a.getServerNames().empty() && b.getServerNames().empty())
|
||||
throw std::runtime_error("duplicate default server for " + la->host + ":" +
|
||||
std::to_string(la->port));
|
||||
|
||||
for (std::vector<std::string>::const_iterator na = a.getServerNames().begin();
|
||||
na != a.getServerNames().end(); ++na) {
|
||||
for (std::vector<std::string>::const_iterator nb = b.getServerNames().begin();
|
||||
nb != b.getServerNames().end(); ++nb) {
|
||||
if (*na == *nb)
|
||||
throw std::runtime_error("duplicate server_name '" + *na + "' on " +
|
||||
la->host + ":" + std::to_string(la->port));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
/*********************************/
|
||||
/* */
|
||||
/* 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;
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/*********************************/
|
||||
/* */
|
||||
/* o.riabenkyi@gmain.com */
|
||||
/* */
|
||||
/*********************************/
|
||||
|
||||
#include "config/LocationConfig.hpp"
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
LocationConfig::LocationConfig()
|
||||
: _path(), _root(), _methods(), _autoindex(false), _index(), _return(),
|
||||
_uploadStore(), _cgiExtension(), _cgiPass() {}
|
||||
|
||||
void LocationConfig::setPath(const std::string &path) { _path = path; }
|
||||
|
||||
void LocationConfig::setRoot(const std::string &root) { _root = root; }
|
||||
|
||||
void LocationConfig::setMethods(const std::vector<std::string> &methods) { _methods = methods; }
|
||||
|
||||
void LocationConfig::setAutoindex(bool autoindex) { _autoindex = autoindex; }
|
||||
|
||||
void LocationConfig::setIndex(const std::string &index) { _index = index; }
|
||||
|
||||
void LocationConfig::setReturn(int code, const std::string &url) {
|
||||
Redirect redirect;
|
||||
redirect.code = code;
|
||||
redirect.url = url;
|
||||
_return = redirect;
|
||||
}
|
||||
|
||||
void LocationConfig::setUploadStore(const std::string &uploadStore) { _uploadStore = uploadStore; }
|
||||
|
||||
void LocationConfig::setCgiExtension(const std::string &extension) { _cgiExtension = extension; }
|
||||
|
||||
void LocationConfig::setCgiPass(const std::string &pass) { _cgiPass = pass; }
|
||||
|
||||
const std::string &LocationConfig::getPath() const { return _path; }
|
||||
|
||||
const std::optional<std::string> &LocationConfig::getRoot() const { return _root; }
|
||||
|
||||
const std::vector<std::string> &LocationConfig::getMethods() const { return _methods; }
|
||||
|
||||
bool LocationConfig::getAutoindex() const { return _autoindex; }
|
||||
|
||||
const std::optional<std::string> &LocationConfig::getIndex() const { return _index; }
|
||||
|
||||
const std::optional<LocationConfig::Redirect> &LocationConfig::getReturn() const { return _return; }
|
||||
|
||||
const std::optional<std::string> &LocationConfig::getUploadStore() const { return _uploadStore; }
|
||||
|
||||
const std::optional<std::string> &LocationConfig::getCgiExtension() const { return _cgiExtension; }
|
||||
|
||||
const std::optional<std::string> &LocationConfig::getCgiPass() const { return _cgiPass; }
|
||||
|
||||
void LocationConfig::validate() const {
|
||||
if (_path.empty())
|
||||
throw std::runtime_error("location block must have a path");
|
||||
|
||||
for (std::vector<std::string>::const_iterator it = _methods.begin(); it != _methods.end(); ++it) {
|
||||
if (*it != "GET" && *it != "POST" && *it != "DELETE")
|
||||
throw std::runtime_error("unsupported HTTP method '" + *it + "' in location '" + _path + "'");
|
||||
}
|
||||
|
||||
if (_return.has_value() && (_return->code < 300 || _return->code > 399))
|
||||
throw std::runtime_error("'return' code in location '" + _path + "' must be a 3xx redirect code");
|
||||
|
||||
if (_cgiExtension.has_value() != _cgiPass.has_value())
|
||||
throw std::runtime_error("location '" + _path +
|
||||
"' must set both 'cgi_extension' and 'cgi_pass', or neither");
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/*********************************/
|
||||
/* */
|
||||
/* o.riabenkyi@gmain.com */
|
||||
/* */
|
||||
/*********************************/
|
||||
|
||||
#include "config/ServerConfig.hpp"
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
namespace {
|
||||
// Hard code-level default, used when 'client_max_body_size' is not set.
|
||||
const size_t kDefaultClientMaxBodySize = 1000000;
|
||||
} // namespace
|
||||
|
||||
ServerConfig::ServerConfig()
|
||||
: _listens(), _serverNames(), _root(), _clientMaxBodySize(kDefaultClientMaxBodySize),
|
||||
_errorPages(), _locations() {}
|
||||
|
||||
void ServerConfig::addListen(const std::string &host, int port) {
|
||||
Listen listen;
|
||||
listen.host = host;
|
||||
listen.port = port;
|
||||
_listens.push_back(listen);
|
||||
}
|
||||
|
||||
void ServerConfig::addServerName(const std::string &name) { _serverNames.push_back(name); }
|
||||
|
||||
void ServerConfig::setRoot(const std::string &root) { _root = root; }
|
||||
|
||||
void ServerConfig::setClientMaxBodySize(size_t size) { _clientMaxBodySize = size; }
|
||||
|
||||
void ServerConfig::addErrorPage(int code, const std::string &path) { _errorPages[code] = path; }
|
||||
|
||||
void ServerConfig::addLocation(const LocationConfig &location) { _locations.push_back(location); }
|
||||
|
||||
const std::vector<ServerConfig::Listen> &ServerConfig::getListens() const { return _listens; }
|
||||
|
||||
const std::vector<std::string> &ServerConfig::getServerNames() const { return _serverNames; }
|
||||
|
||||
const std::optional<std::string> &ServerConfig::getRoot() const { return _root; }
|
||||
|
||||
size_t ServerConfig::getClientMaxBodySize() const { return _clientMaxBodySize; }
|
||||
|
||||
const std::map<int, std::string> &ServerConfig::getErrorPages() const { return _errorPages; }
|
||||
|
||||
const std::vector<LocationConfig> &ServerConfig::getLocations() const { return _locations; }
|
||||
|
||||
const std::string &ServerConfig::resolveRoot(const LocationConfig &location) const {
|
||||
if (location.getRoot().has_value())
|
||||
return *location.getRoot();
|
||||
if (_root.has_value())
|
||||
return *_root;
|
||||
throw std::runtime_error("no 'root' available for location '" + location.getPath() + "'");
|
||||
}
|
||||
|
||||
void ServerConfig::validate() const {
|
||||
if (_listens.empty())
|
||||
throw std::runtime_error("server block must have at least one 'listen' directive");
|
||||
for (std::vector<Listen>::const_iterator it = _listens.begin(); it != _listens.end(); ++it) {
|
||||
if (it->port <= 0 || it->port > 65535)
|
||||
throw std::runtime_error("invalid port number in 'listen' directive");
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < _locations.size(); ++i) {
|
||||
_locations[i].validate();
|
||||
|
||||
if (!_locations[i].getRoot().has_value() && !_root.has_value())
|
||||
throw std::runtime_error("no 'root' defined for location '" + _locations[i].getPath() +
|
||||
"' and the server block has no fallback root either");
|
||||
|
||||
for (size_t j = i + 1; j < _locations.size(); ++j) {
|
||||
if (_locations[i].getPath() == _locations[j].getPath())
|
||||
throw std::runtime_error("duplicate location path '" + _locations[i].getPath() + "'");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user