first approximation of the parser

This commit is contained in:
2026-09-08 11:06:04 +02:00
parent b15ce130f9
commit bdb434d9d5
13 changed files with 910 additions and 5 deletions
+20 -4
View File
@@ -50,14 +50,30 @@ CLANG_STDLIB ?= -stdlib=libc++
# find returns files in filesystem order, which is not the same on every
# machine. sort keeps the compile and link lines stable, so build logs and
# clang-tidy output stay comparable.
SRCS := $(sort $(shell find $(SRC_DIR) -type f -name '*.cpp'))
OBJS := $(SRCS:$(SRC_DIR)/%.cpp=$(OBJ_DIR)/%.o)
#SRCS := $(sort $(shell find $(SRC_DIR) -type f -name '*.cpp'))
SRCS := src/Log.cpp src/main.cpp src/Server.cpp \
src/config/Config.cpp src/config/ConfigurationParser.cpp src/config/LocationConfig.cpp src/config/ServerConfig.cpp \
src/event/Epoll.cpp src/event/EventToken.cpp \
src/net/ByteBuffer.cpp src/net/Connection.cpp src/net/FileDescriptor.cpp src/net/Listener.cpp src/net/PauseMask.cpp src/net/SlotPool.cpp
# src/http/HttpStatus.cpp \
OBJS := $(SRCS:$(SRC_DIR)/%.cpp=$(OBJ_DIR)/%.o)
# Make only knows that obj/main.o comes from src/main.cpp, so editing Log.hpp
# would leave the old main.o in place. Each .d adds the missing line, obj/main.o
# also depends on include/Log.hpp, and -include at the bottom pulls them in.
DEPS := $(OBJS:.o=.d)
HDRS := $(sort $(shell find $(INC_DIR) $(SRC_DIR) -type f \
\( -name '*.hpp' -o -name '*.tpp' \)))
HDRS := include/config/Config.hpp include/config/ConfigurationParser.hpp include/config/LocationConfig.hpp include/config/ServerConfig.hpp \
include/event/Epoll.hpp include/event/EventToken.hpp \
include/http/HttpStatus.hpp \
include/net/ByteBuffer.hpp include/net/Connection.hpp include/net/FileDescriptor.hpp include/net/Listener.hpp include/net/PauseMask.hpp include/net/SlotPool.hpp \
include/Log.hpp include/Result.hpp include/Server.hpp \
#HDRS := $(sort $(shell find $(INC_DIR) $(SRC_DIR) -type f \
# \( -name '*.hpp' -o -name '*.tpp' \)))
all: $(NAME)
+64
View File
@@ -0,0 +1,64 @@
# Default webserv configuration.
# Any '#' on a line means: everything from there to end of line is ignored.
#
# Syntax rules:
# - one line is exactly one thing: a block opener ("server {" /
# "location /path {"), a lone closing "}", or a single directive
# - there is no ';' terminator, so a closing "}" must sit alone on its
# own line and a directive's values are just the rest of its line
server {
listen 127.0.0.1:8080
listen 127.0.0.1:8081
server_name example.com www.example.com
root ./www
client_max_body_size 1M
error_page 404 ./www/errors/404.html
error_page 500 502 503 504 ./www/errors/50x.html
location / {
methods GET POST
index index.html
autoindex off
}
location /files {
methods GET
autoindex on
}
location /upload {
methods POST DELETE
upload_store ./www/uploads
}
location /old {
return 301 /
}
location /cgi-bin/python {
methods GET POST
cgi_extension .py
cgi_pass /usr/bin/python3
}
location /cgi-bin/php {
methods GET POST
cgi_extension .php
cgi_pass /usr/bin/php-cgi
}
}
server {
listen 0.0.0.0:8082
server_name second.local
root ./www2
location / {
methods GET
index index.html
}
}
+44
View File
@@ -0,0 +1,44 @@
server {
listen 127.0.0.1:8000
server_name example.com www.example.com
root ./www
client_max_body_size 1M
error_page 404 ./www/errors/404.html
error_page 500 502 503 504 ./www/errors/50x.html
location / {
methods GET POST
index index.html
autoindex off
}
location /files {
methods GET
autoindex on
}
location /upload {
methods POST DELETE
upload_store ./www/uploads
}
location /old {
return 301 /
}
location /cgi-bin/python {
methods GET POST
cgi_extension .py
cgi_pass /usr/bin/python3
}
location /cgi-bin/php {
methods GET POST
cgi_extension .php
cgi_pass /usr/bin/php-cgi
}
}
+56
View File
@@ -0,0 +1,56 @@
server {
listen 127.0.0.1:8000
listen 127.0.0.1:8081
server_name example.com www.example.com
root ./www
client_max_body_size 1M
error_page 404 ./www/errors/404.html
error_page 500 502 503 504 ./www/errors/50x.html
location / {
methods GET POST
index index.html
autoindex off
}
location /files {
methods GET
autoindex on
}
location /upload {
methods POST DELETE
upload_store ./www/uploads
}
location /old {
return 301 /
}
location /cgi-bin/python {
methods GET POST
cgi_extension .py
cgi_pass /usr/bin/python3
}
location /cgi-bin/php {
methods GET POST
cgi_extension .php
cgi_pass /usr/bin/php-cgi
}
}
server {
listen 0.0.0.0:8000
server_name second.local
root ./www2
location / {
methods GET
index index.html
}
}
+30
View File
@@ -0,0 +1,30 @@
/*********************************/
/* */
/* o.riabenkyi@gmain.com */
/* */
/*********************************/
#ifndef CONFIG_HPP
#define CONFIG_HPP
#include <vector>
#include "ServerConfig.hpp"
// Holds every server block parsed from the configuration file and validates
// them, both individually and against each other.
class Config {
public:
Config();
void addServer(const ServerConfig &server);
const std::vector<ServerConfig> &getServers() const;
// Throws std::runtime_error with a descriptive message when invalid.
void validate() const;
private:
std::vector<ServerConfig> _servers;
};
#endif
+66
View File
@@ -0,0 +1,66 @@
/*********************************/
/* */
/* o.riabenkyi@gmain.com */
/* */
/*********************************/
#ifndef CONFIGURATIONPARSER_HPP
#define CONFIGURATIONPARSER_HPP
#include <cstddef>
#include <string>
#include <vector>
#include "Config.hpp"
#include "LocationConfig.hpp"
#include "ServerConfig.hpp"
// Reads a webserv configuration file and turns it into a validated Config.
//
// Syntax (see webserv_design_notes.md):
// - one logical line is exactly one syntactic unit: a block opener
// ("server {" / "location /path {"), a lone closing "}", or a single
// directive followed by its values
// - '#' starts a comment that runs to the end of the line, stripped
// before tokenizing
// - there is no ';' terminator - a directive's values are simply the
// remaining tokens on its line
// - a closing "}" must appear alone on its own line
class ConfigurationParser {
public:
ConfigurationParser();
~ConfigurationParser();
// Reads the file at `path`, parses it and returns a validated Config.
// Throws std::runtime_error on any syntax or validation error.
Config parse(const std::string &path);
private:
std::vector<std::vector<std::string> > _lines;
size_t _pos;
std::string readFile(const std::string &path) const;
void tokenizeLines(const std::string &content);
static std::string stripComment(const std::string &line);
static std::vector<std::string> splitTokens(const std::string &line);
static std::string joinTokens(const std::vector<std::string> &tokens);
bool hasNext() const;
const std::vector<std::string> &peek() const;
std::vector<std::string> next();
ServerConfig parseServerBlock();
LocationConfig parseLocationBlock(const std::string &path);
void applyServerDirective(ServerConfig &server, const std::string &name,
const std::vector<std::string> &values);
void applyLocationDirective(LocationConfig &location, const std::string &name,
const std::vector<std::string> &values);
static int toInt(const std::string &s);
static size_t parseSize(const std::string &s);
ConfigurationParser(const ConfigurationParser &other);
ConfigurationParser &operator=(const ConfigurationParser &other);
};
#endif
+73
View File
@@ -0,0 +1,73 @@
/*********************************/
/* */
/* o.riabenkyi@gmain.com */
/* */
/*********************************/
#ifndef LOCATIONCONFIG_HPP
#define LOCATIONCONFIG_HPP
#include <optional>
#include <string>
#include <vector>
// One "location { ... }" block: routing rules for one path prefix.
//
// | Directive | Purpose |
// | `methods` | permitted HTTP methods |
// | `root` | root directory on the disk |
// | `autoindex` | directory listing on/off |
// | `index` | default file for the directory |
// | `return` | HTTP redirect (status code + URL) |
// | `upload_store` | path for storing uploaded files |
// | `cgi_extension`| file extension → CGI |
// | `cgi_pass` | path to the CGI executable file |
class LocationConfig {
public:
struct Redirect {
int code;
std::string url;
};
LocationConfig();
void setPath(const std::string &path);
void setRoot(const std::string &root);
void setMethods(const std::vector<std::string> &methods);
void setAutoindex(bool autoindex);
void setIndex(const std::string &index);
void setReturn(int code, const std::string &url);
void setUploadStore(const std::string &uploadStore);
void setCgiExtension(const std::string &extension);
void setCgiPass(const std::string &pass);
const std::string &getPath() const;
const std::optional<std::string> &getRoot() const;
const std::vector<std::string> &getMethods() const;
bool getAutoindex() const;
const std::optional<std::string> &getIndex() const;
const std::optional<Redirect> &getReturn() const;
const std::optional<std::string> &getUploadStore() const;
const std::optional<std::string> &getCgiExtension() const;
const std::optional<std::string> &getCgiPass() const;
// Throws std::runtime_error with a descriptive message when invalid.
// Does not check root inheritance - the owning ServerConfig does that,
// since it alone knows whether a server-level root is available.
void validate() const;
private:
std::string _path;
std::optional<std::string> _root;
std::vector<std::string> _methods;
bool _autoindex;
std::optional<std::string> _index;
std::optional<Redirect> _return;
std::optional<std::string> _uploadStore;
std::optional<std::string> _cgiExtension;
std::optional<std::string> _cgiPass;
};
#endif
+58
View File
@@ -0,0 +1,58 @@
/*********************************/
/* */
/* o.riabenkyi@gmain.com */
/* */
/*********************************/
#ifndef SERVERCONFIG_HPP
#define SERVERCONFIG_HPP
#include <cstddef>
#include <map>
#include <optional>
#include <string>
#include <vector>
#include "LocationConfig.hpp"
// One "server { ... }" block: everything needed to run a single virtual server.
class ServerConfig {
public:
struct Listen {
std::string host;
int port;
};
ServerConfig();
void addListen(const std::string &host, int port);
void addServerName(const std::string &name);
void setRoot(const std::string &root);
void setClientMaxBodySize(size_t size);
void addErrorPage(int code, const std::string &path);
void addLocation(const LocationConfig &location);
const std::vector<Listen> &getListens() const;
const std::vector<std::string> &getServerNames() const;
const std::optional<std::string> &getRoot() const;
size_t getClientMaxBodySize() const;
const std::map<int, std::string> &getErrorPages() const;
const std::vector<LocationConfig> &getLocations() const;
// Returns the location's own root if set, otherwise this server's root.
// Only meaningful once validate() has confirmed one of the two exists.
const std::string &resolveRoot(const LocationConfig &location) const;
// Throws std::runtime_error with a descriptive message when invalid.
void validate() const;
private:
std::vector<Listen> _listens;
std::vector<std::string> _serverNames;
std::optional<std::string> _root;
size_t _clientMaxBodySize;
std::map<int, std::string> _errorPages;
std::vector<LocationConfig> _locations;
};
#endif
+79
View File
@@ -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));
}
}
}
}
}
}
}
+249
View File
@@ -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;
}
+71
View File
@@ -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");
}
+77
View File
@@ -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() + "'");
}
}
}
+23 -1
View File
@@ -24,7 +24,9 @@
#include "Log.hpp"
#include "Server.hpp"
constexpr std::string_view DEFAULT_CONFIG_PATH = "conf/default.conf";
#include "config/ConfigurationParser.hpp"
constexpr std::string_view DEFAULT_CONFIG_PATH = "conf/_default.conf";
namespace {
@@ -54,6 +56,26 @@ int main(int argc, char** argv) {
webserv::log::info("configuration file {}", configPath);
try {
ConfigurationParser parser;
Config config = parser.parse(std::string(configPath));
const std::vector<ServerConfig> &servers = config.getServers();
std::cout << "Parsed " << servers.size() << " server block(s) from " << configPath << ":\n";
for (size_t i = 0; i < servers.size(); ++i) {
const ServerConfig &server = servers[i];
std::cout << " server " << i << ":";
const std::vector<ServerConfig::Listen> &listens = server.getListens();
for (size_t l = 0; l < listens.size(); ++l)
std::cout << ' ' << listens[l].host << ':' << listens[l].port;
std::cout << " root=" << server.getRoot().value_or("(inherited per-location)")
<< " locations=" << server.getLocations().size() << '\n';
}
} catch (const std::exception &e) {
std::cerr << "Configuration error: " << e.what() << '\n';
return 1;
}
// Ignore SIGPIPE so writing to a closed socket or pipe reports an error
// instead of terminating the server. Other clients can then continue.
if (::signal(SIGPIPE, SIG_IGN) == SIG_ERR) {