first approximation of the parser
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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() + "'");
|
||||
}
|
||||
}
|
||||
}
|
||||
+23
-1
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user