diff --git a/Makefile b/Makefile index f60a7a2..9c33a69 100644 --- a/Makefile +++ b/Makefile @@ -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) diff --git a/conf/_default.conf b/conf/_default.conf new file mode 100644 index 0000000..4669e02 --- /dev/null +++ b/conf/_default.conf @@ -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 + } +} diff --git a/conf/_oneserver.conf b/conf/_oneserver.conf new file mode 100644 index 0000000..922347d --- /dev/null +++ b/conf/_oneserver.conf @@ -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 + } +} + diff --git a/conf/_wrongPort.conf b/conf/_wrongPort.conf new file mode 100644 index 0000000..966eeca --- /dev/null +++ b/conf/_wrongPort.conf @@ -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 + } +} diff --git a/include/config/Config.hpp b/include/config/Config.hpp new file mode 100644 index 0000000..3eee84f --- /dev/null +++ b/include/config/Config.hpp @@ -0,0 +1,30 @@ +/*********************************/ +/* */ +/* o.riabenkyi@gmain.com */ +/* */ +/*********************************/ + +#ifndef CONFIG_HPP +#define CONFIG_HPP + +#include + +#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 &getServers() const; + + // Throws std::runtime_error with a descriptive message when invalid. + void validate() const; + +private: + std::vector _servers; +}; + +#endif \ No newline at end of file diff --git a/include/config/ConfigurationParser.hpp b/include/config/ConfigurationParser.hpp new file mode 100644 index 0000000..032b6c3 --- /dev/null +++ b/include/config/ConfigurationParser.hpp @@ -0,0 +1,66 @@ +/*********************************/ +/* */ +/* o.riabenkyi@gmain.com */ +/* */ +/*********************************/ + +#ifndef CONFIGURATIONPARSER_HPP +#define CONFIGURATIONPARSER_HPP + +#include +#include +#include + +#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 > _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 splitTokens(const std::string &line); + static std::string joinTokens(const std::vector &tokens); + + bool hasNext() const; + const std::vector &peek() const; + std::vector next(); + + ServerConfig parseServerBlock(); + LocationConfig parseLocationBlock(const std::string &path); + void applyServerDirective(ServerConfig &server, const std::string &name, + const std::vector &values); + void applyLocationDirective(LocationConfig &location, const std::string &name, + const std::vector &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 diff --git a/include/config/LocationConfig.hpp b/include/config/LocationConfig.hpp new file mode 100644 index 0000000..5a27c65 --- /dev/null +++ b/include/config/LocationConfig.hpp @@ -0,0 +1,73 @@ +/*********************************/ +/* */ +/* o.riabenkyi@gmain.com */ +/* */ +/*********************************/ + +#ifndef LOCATIONCONFIG_HPP +#define LOCATIONCONFIG_HPP + +#include +#include +#include + +// 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 &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 &getRoot() const; + const std::vector &getMethods() const; + bool getAutoindex() const; + const std::optional &getIndex() const; + const std::optional &getReturn() const; + const std::optional &getUploadStore() const; + const std::optional &getCgiExtension() const; + const std::optional &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 _root; + std::vector _methods; + bool _autoindex; + std::optional _index; + std::optional _return; + std::optional _uploadStore; + std::optional _cgiExtension; + std::optional _cgiPass; +}; + +#endif diff --git a/include/config/ServerConfig.hpp b/include/config/ServerConfig.hpp new file mode 100644 index 0000000..7d21cb0 --- /dev/null +++ b/include/config/ServerConfig.hpp @@ -0,0 +1,58 @@ +/*********************************/ +/* */ +/* o.riabenkyi@gmain.com */ +/* */ +/*********************************/ + +#ifndef SERVERCONFIG_HPP +#define SERVERCONFIG_HPP + +#include +#include +#include +#include +#include + +#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 &getListens() const; + const std::vector &getServerNames() const; + const std::optional &getRoot() const; + size_t getClientMaxBodySize() const; + const std::map &getErrorPages() const; + const std::vector &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 _listens; + std::vector _serverNames; + std::optional _root; + size_t _clientMaxBodySize; + std::map _errorPages; + std::vector _locations; +}; + +#endif diff --git a/src/config/Config.cpp b/src/config/Config.cpp new file mode 100644 index 0000000..e2f5471 --- /dev/null +++ b/src/config/Config.cpp @@ -0,0 +1,79 @@ +/*********************************/ +/* */ +/* o.riabenkyi@gmain.com */ +/* */ +/*********************************/ + +#include "config/Config.hpp" + +#include + +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 &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::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::const_iterator la = a.getListens().begin(); + la != a.getListens().end(); ++la) { + for (std::vector::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::const_iterator na = a.getServerNames().begin(); + na != a.getServerNames().end(); ++na) { + for (std::vector::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)); + } + } + } + } + } + } +} diff --git a/src/config/ConfigurationParser.cpp b/src/config/ConfigurationParser.cpp new file mode 100644 index 0000000..3c07ea6 --- /dev/null +++ b/src/config/ConfigurationParser.cpp @@ -0,0 +1,249 @@ +/*********************************/ +/* */ +/* 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; +} diff --git a/src/config/LocationConfig.cpp b/src/config/LocationConfig.cpp new file mode 100644 index 0000000..228db09 --- /dev/null +++ b/src/config/LocationConfig.cpp @@ -0,0 +1,71 @@ +/*********************************/ +/* */ +/* o.riabenkyi@gmain.com */ +/* */ +/*********************************/ + +#include "config/LocationConfig.hpp" + +#include + +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 &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 &LocationConfig::getRoot() const { return _root; } + +const std::vector &LocationConfig::getMethods() const { return _methods; } + +bool LocationConfig::getAutoindex() const { return _autoindex; } + +const std::optional &LocationConfig::getIndex() const { return _index; } + +const std::optional &LocationConfig::getReturn() const { return _return; } + +const std::optional &LocationConfig::getUploadStore() const { return _uploadStore; } + +const std::optional &LocationConfig::getCgiExtension() const { return _cgiExtension; } + +const std::optional &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::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"); +} diff --git a/src/config/ServerConfig.cpp b/src/config/ServerConfig.cpp new file mode 100644 index 0000000..6e72ac5 --- /dev/null +++ b/src/config/ServerConfig.cpp @@ -0,0 +1,77 @@ +/*********************************/ +/* */ +/* o.riabenkyi@gmain.com */ +/* */ +/*********************************/ + +#include "config/ServerConfig.hpp" + +#include + +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::getListens() const { return _listens; } + +const std::vector &ServerConfig::getServerNames() const { return _serverNames; } + +const std::optional &ServerConfig::getRoot() const { return _root; } + +size_t ServerConfig::getClientMaxBodySize() const { return _clientMaxBodySize; } + +const std::map &ServerConfig::getErrorPages() const { return _errorPages; } + +const std::vector &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::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() + "'"); + } + } +} diff --git a/src/main.cpp b/src/main.cpp index 584b984..d7ff51f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -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 &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 &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) {