4 Commits

Author SHA1 Message Date
oriabenkyi 2114e11f90 reactie van server 2026-09-13 13:13:33 +02:00
oriabenkyi 72d072bb6c one more parser 2026-09-12 08:29:44 +02:00
oriabenkyi 751b54db18 add lisener for socket 2026-09-10 08:27:06 +02:00
oriabenkyi bdb434d9d5 first approximation of the parser 2026-09-08 11:06:04 +02:00
23 changed files with 1441 additions and 56 deletions
+19 -4
View File
@@ -50,14 +50,29 @@ 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/ListenerPlan.cpp src/net/PauseMask.cpp src/net/SlotPool.cpp \
src/http/Request.cpp src/http/RequestParser.cpp src/http/Response.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/http/Request.hpp include/http/RequestParser.hpp include/http/Response.hpp \
include/net/ByteBuffer.hpp include/net/Connection.hpp include/net/FileDescriptor.hpp include/net/Listener.hpp include/net/ListenerPlan.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@gmail.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@gmail.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@gmail.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@gmail.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
+40
View File
@@ -0,0 +1,40 @@
/*********************************/
/* */
/* o.riabenkyi@gmail.com */
/* */
/*********************************/
#ifndef REQUEST_HPP
#define REQUEST_HPP
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
namespace webserv {
enum class Method { Get, Post, Delete };
const char* toString(Method method) noexcept;
bool headerNameEquals(std::string_view a, std::string_view b) noexcept;
enum class HttpVersion { Http10, Http11 };
struct Request {
Method method;
std::string target;
HttpVersion version;
std::vector<std::pair<std::string, std::string>> headers;
std::vector<char> body;
std::optional<std::string_view> header(std::string_view name) const noexcept;
};
}
#endif
+29
View File
@@ -0,0 +1,29 @@
/*********************************/
/* */
/* o.riabenkyi@gmail.com */
/* */
/*********************************/
#ifndef REQUESTPARSER_HPP
#define REQUESTPARSER_HPP
#include <cstddef>
#include <optional>
#include <span>
#include "Result.hpp"
#include "http/Request.hpp"
namespace webserv {
struct ParsedRequest {
Request request;
std::size_t consumed;
};
[[nodiscard]] std::optional<Result<ParsedRequest>> parseRequest(
std::span<const char> data, std::size_t limit);
} // namespace webserv
#endif
+30
View File
@@ -0,0 +1,30 @@
/*********************************/
/* */
/* o.riabenkyi@gmail.com */
/* */
/*********************************/
#ifndef RESPONSE_HPP
#define RESPONSE_HPP
#include <string>
#include <utility>
#include <vector>
#include "http/HttpStatus.hpp"
namespace webserv {
const char* reasonPhrase(HttpStatus status) noexcept;
struct Response {
HttpStatus status;
std::vector<std::pair<std::string, std::string>> headers;
std::vector<char> body;
};
std::vector<char> serialize(const Response& response);
} // namespace webserv
#endif
-12
View File
@@ -29,15 +29,9 @@ class Connection final {
std::uint32_t desiredEvents;
};
// ByteBuffer::CAPACITY is 64 KiB.
// At most 16 KiB are copied per 48 KiB appended: 16 / 48 = 1 / 3.
// A lower threshold reduces copying but leaves less unread work available.
// The 16 KiB value is a fixed compromise, not a measured optimum.
static constexpr std::size_t LOW_WATERMARK = 16uz * 1024;
explicit Connection(FileDescriptor socket);
// Each Connection has a unique identity and remains in its original slot.
Connection(const Connection&) = delete;
Connection& operator=(const Connection&) = delete;
Connection(Connection&&) = delete;
@@ -52,14 +46,8 @@ class Connection final {
void close() noexcept { socket_.reset(); }
private:
// recv() returning zero means the client has finished sending data.
// readClosed_ records this. receive() still returns true because this
// is not an error and the socket may still send its buffered response.
// Only a recv() error returns false.
bool receive();
bool transmit();
// TEMP echo path: queues received bytes unchanged for sending back to
// the client.
void forwardReceivedBytes();
FileDescriptor socket_;
+60
View File
@@ -0,0 +1,60 @@
/*********************************/
/* */
/* o.riabenkyi@gmail.com */
/* */
/*********************************/
#ifndef LISTENERPLAN_HPP
#define LISTENERPLAN_HPP
#include <sys/socket.h>
#include <exception>
#include <string>
#include <vector>
#include "config/Config.hpp"
namespace webserv {
// Turns a validated Config into the deduplicated set of sockets the server
// must bind. Two server blocks may list the same host:port to share one
// socket (virtual hosting, picked apart later by server_name), so this
// groups listen directives by unique host:port instead of binding one
// socket per directive, which would make the second bind() fail.
//
// The ServerConfig pointers in routes() point into the Config passed to
// build(); that Config must outlive the ListenerPlan.
class ListenerPlan final {
public:
class Error : public std::exception {
public:
explicit Error(std::string message);
const char* what() const noexcept override;
private:
std::string msg_;
};
static ListenerPlan build(const Config& config);
const std::vector<sockaddr_storage>& endpoints() const noexcept {
return endpoints_;
}
// routes()[i] lists, in config order, every ServerConfig reachable
// through endpoints()[i]. Not consumed yet: it becomes the input to
// Host-header based virtual host selection once HTTP request routing
// exists.
const std::vector<std::vector<const ServerConfig*>>& routes() const noexcept {
return routes_;
}
private:
std::vector<sockaddr_storage> endpoints_;
std::vector<std::vector<const ServerConfig*>> routes_;
};
} // namespace webserv
#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() + "'");
}
}
}
+44
View File
@@ -0,0 +1,44 @@
/* ************************************************************************** */
/* */
/* ::: :::::::: */
/* Request.cpp :+: :+: :+: */
/* +:+ +:+ +:+ */
/* +#+#+#+#+#+ +#+ */
/* */
/* ************************************************************************** */
#include "http/Request.hpp"
#include <algorithm>
#include <cctype>
namespace webserv {
const char* toString(Method method) noexcept {
switch (method) {
case Method::Get:
return "GET";
case Method::Post:
return "POST";
case Method::Delete:
return "DELETE";
}
return "GET";
}
bool headerNameEquals(std::string_view a, std::string_view b) noexcept {
return std::ranges::equal(a, b, [](unsigned char x, unsigned char y) {
return std::tolower(x) == std::tolower(y);
});
}
std::optional<std::string_view> Request::header(std::string_view name) const noexcept {
for (const auto& [fieldName, value] : headers) {
if (headerNameEquals(fieldName, name)) {
return std::string_view(value);
}
}
return std::nullopt;
}
} // namespace webserv
+171
View File
@@ -0,0 +1,171 @@
/* ************************************************************************** */
/* */
/* ::: :::::::: */
/* RequestParser.cpp :+: :+: :+: */
/* +:+ +:+ +:+ */
/* +#+#+#+#+#+ +#+ */
/* */
/* ************************************************************************** */
#include "http/RequestParser.hpp"
#include <array>
#include <charconv>
#include <string>
#include <string_view>
#include <utility>
#include "http/HttpStatus.hpp"
namespace webserv {
namespace {
std::optional<Method> parseMethod(std::string_view token) {
if (token == "GET") return Method::Get;
if (token == "POST") return Method::Post;
if (token == "DELETE") return Method::Delete;
return std::nullopt;
}
std::optional<HttpVersion> parseVersion(std::string_view token) {
if (token == "HTTP/1.1") return HttpVersion::Http11;
if (token == "HTTP/1.0") return HttpVersion::Http10;
return std::nullopt;
}
std::string_view trim(std::string_view value) {
while (!value.empty() && (value.front() == ' ' || value.front() == '\t'))
value.remove_prefix(1);
while (!value.empty() && (value.back() == ' ' || value.back() == '\t'))
value.remove_suffix(1);
return value;
}
std::optional<std::array<std::string_view, 3>> splitRequestLine(std::string_view line) {
const std::size_t firstSpace = line.find(' ');
if (firstSpace == std::string_view::npos) return std::nullopt;
const std::size_t secondSpace = line.find(' ', firstSpace + 1);
if (secondSpace == std::string_view::npos) return std::nullopt;
if (line.find(' ', secondSpace + 1) != std::string_view::npos) return std::nullopt;
return std::array<std::string_view, 3>{
line.substr(0, firstSpace),
line.substr(firstSpace + 1, secondSpace - firstSpace - 1),
line.substr(secondSpace + 1)};
}
std::optional<std::pair<std::string, std::string>> parseHeaderLine(std::string_view line) {
const std::size_t colon = line.find(':');
if (colon == std::string_view::npos || colon == 0) return std::nullopt;
const std::string_view name = line.substr(0, colon);
if (name.find(' ') != std::string_view::npos || name.find('\t') != std::string_view::npos)
return std::nullopt;
const std::string_view value = trim(line.substr(colon + 1));
return std::pair<std::string, std::string>(std::string(name), std::string(value));
}
std::optional<std::size_t> parseContentLength(std::string_view value) {
if (value.empty()) return std::nullopt;
std::size_t result = 0;
const auto parsed = std::from_chars(value.data(), value.data() + value.size(), result);
if (parsed.ec != std::errc{} || parsed.ptr != value.data() + value.size())
return std::nullopt;
return result;
}
} // namespace
std::optional<Result<ParsedRequest>> parseRequest(std::span<const char> data, std::size_t limit) {
const std::string_view view(data.data(), data.size());
const std::size_t headEnd = view.find("\r\n\r\n");
if (headEnd == std::string_view::npos) {
if (data.size() >= limit)
return Result<ParsedRequest>(std::unexpected(HttpStatus::RequestHeaderFieldsTooLarge));
return std::nullopt;
}
const std::size_t headBytes = headEnd + 4;
if (headBytes > limit)
return Result<ParsedRequest>(std::unexpected(HttpStatus::RequestHeaderFieldsTooLarge));
std::string_view remaining = view.substr(0, headEnd);
const std::size_t lineEnd = remaining.find("\r\n");
const std::string_view requestLine = remaining.substr(0, lineEnd);
remaining = (lineEnd == std::string_view::npos) ? std::string_view()
: remaining.substr(lineEnd + 2);
const auto fields = splitRequestLine(requestLine);
if (!fields.has_value())
return Result<ParsedRequest>(std::unexpected(HttpStatus::BadRequest));
const auto method = parseMethod((*fields)[0]);
if (!method.has_value())
return Result<ParsedRequest>(std::unexpected(HttpStatus::NotImplemented));
const std::string_view target = (*fields)[1];
if (target.empty() || target.front() != '/')
return Result<ParsedRequest>(std::unexpected(HttpStatus::BadRequest));
const auto version = parseVersion((*fields)[2]);
if (!version.has_value())
return Result<ParsedRequest>(std::unexpected(HttpStatus::HttpVersionNotSupported));
Request request;
request.method = *method;
request.target = std::string(target);
request.version = *version;
while (!remaining.empty()) {
const std::size_t next = remaining.find("\r\n");
const std::string_view headerLine = remaining.substr(0, next);
remaining = (next == std::string_view::npos) ? std::string_view()
: remaining.substr(next + 2);
auto field = parseHeaderLine(headerLine);
if (!field.has_value())
return Result<ParsedRequest>(std::unexpected(HttpStatus::BadRequest));
request.headers.push_back(std::move(*field));
}
if (request.version == HttpVersion::Http11 && !request.header("Host").has_value())
return Result<ParsedRequest>(std::unexpected(HttpStatus::BadRequest));
bool hasTransferEncoding = false;
std::optional<std::size_t> contentLength;
for (const auto& [name, value] : request.headers) {
if (headerNameEquals(name, "transfer-encoding")) {
hasTransferEncoding = true;
} else if (headerNameEquals(name, "content-length")) {
const auto parsed = parseContentLength(value);
if (!parsed.has_value())
return Result<ParsedRequest>(std::unexpected(HttpStatus::BadRequest));
if (contentLength.has_value() && *contentLength != *parsed)
return Result<ParsedRequest>(std::unexpected(HttpStatus::BadRequest));
contentLength = parsed;
}
}
if (hasTransferEncoding && contentLength.has_value())
return Result<ParsedRequest>(std::unexpected(HttpStatus::BadRequest));
if (hasTransferEncoding) {
return Result<ParsedRequest>(std::unexpected(HttpStatus::NotImplemented));
}
const std::size_t bodyLength = contentLength.value_or(0);
if (bodyLength > limit - headBytes)
return Result<ParsedRequest>(std::unexpected(HttpStatus::ContentTooLarge));
if (data.size() < headBytes + bodyLength) return std::nullopt;
request.body.assign(data.begin() + static_cast<std::ptrdiff_t>(headBytes),
data.begin() + static_cast<std::ptrdiff_t>(headBytes + bodyLength));
return Result<ParsedRequest>(ParsedRequest{std::move(request), headBytes + bodyLength});
}
} // namespace webserv
+78
View File
@@ -0,0 +1,78 @@
/*********************************/
/* */
/* o.riabenkyi@gmail.com */
/* */
/*********************************/
#include "http/Response.hpp"
#include <format>
namespace webserv {
const char* reasonPhrase(HttpStatus status) noexcept {
switch (status) {
case HttpStatus::Ok:
return "OK";
case HttpStatus::Created:
return "Created";
case HttpStatus::NoContent:
return "No Content";
case HttpStatus::MovedPermanently:
return "Moved Permanently";
case HttpStatus::Found:
return "Found";
case HttpStatus::SeeOther:
return "See Other";
case HttpStatus::TemporaryRedirect:
return "Temporary Redirect";
case HttpStatus::PermanentRedirect:
return "Permanent Redirect";
case HttpStatus::BadRequest:
return "Bad Request";
case HttpStatus::Forbidden:
return "Forbidden";
case HttpStatus::NotFound:
return "Not Found";
case HttpStatus::MethodNotAllowed:
return "Method Not Allowed";
case HttpStatus::RequestTimeout:
return "Request Timeout";
case HttpStatus::LengthRequired:
return "Length Required";
case HttpStatus::ContentTooLarge:
return "Content Too Large";
case HttpStatus::UriTooLong:
return "URI Too Long";
case HttpStatus::RequestHeaderFieldsTooLarge:
return "Request Header Fields Too Large";
case HttpStatus::InternalServerError:
return "Internal Server Error";
case HttpStatus::NotImplemented:
return "Not Implemented";
case HttpStatus::BadGateway:
return "Bad Gateway";
case HttpStatus::ServiceUnavailable:
return "Service Unavailable";
case HttpStatus::GatewayTimeout:
return "Gateway Timeout";
case HttpStatus::HttpVersionNotSupported:
return "HTTP Version Not Supported";
}
return "Unknown Status";
}
std::vector<char> serialize(const Response& response) {
std::string head = std::format("HTTP/1.1 {} {}\r\n", static_cast<int>(response.status),
reasonPhrase(response.status));
for (const auto& [name, value] : response.headers) {
head += std::format("{}: {}\r\n", name, value);
}
head += std::format("Content-Length: {}\r\n\r\n", response.body.size());
std::vector<char> bytes(head.begin(), head.end());
bytes.insert(bytes.end(), response.body.begin(), response.body.end());
return bytes;
}
} // namespace webserv
+29 -26
View File
@@ -10,12 +10,8 @@
/* */
/* ************************************************************************** */
#include <netinet/in.h>
#include <array>
#include <cerrno>
#include <csignal>
#include <cstdint>
#include <cstring>
#include <exception>
#include <iostream>
@@ -24,24 +20,10 @@
#include "Log.hpp"
#include "Server.hpp"
constexpr std::string_view DEFAULT_CONFIG_PATH = "conf/default.conf";
#include "config/ConfigurationParser.hpp"
#include "net/ListenerPlan.hpp"
namespace {
// TEMP: Builds wildcard IPv4 addresses for the fixed echo server ports.
// Parsed configuration will eventually provide resolved listener addresses.
sockaddr_storage anyAddress(std::uint16_t port) {
const sockaddr_in address{.sin_family = AF_INET,
.sin_port = ::htons(port),
.sin_addr = {.s_addr = ::htonl(INADDR_ANY)},
.sin_zero = {}};
sockaddr_storage storage{};
std::memcpy(&storage, &address, sizeof(address));
return storage;
}
} // namespace
constexpr std::string_view DEFAULT_CONFIG_PATH = "conf/_default.conf";
int main(int argc, char** argv) {
if (argc > 2) {
@@ -54,6 +36,30 @@ int main(int argc, char** argv) {
webserv::log::info("configuration file {}", configPath);
// Declared here, outside the parsing try block, because ListenerPlan
// keeps pointers into this Config's ServerConfig blocks and both the
// plan and the running server need it to stay alive past parsing.
Config config;
try {
ConfigurationParser parser;
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) {
@@ -62,11 +68,8 @@ int main(int argc, char** argv) {
}
try {
// TEMP: The echo server listens on three fixed ports until main obtains
// the listener addresses from the configuration file.
const std::array<sockaddr_storage, 3> endpoints{
anyAddress(8000), anyAddress(8001), anyAddress(8002)};
webserv::Server server(endpoints);
const webserv::ListenerPlan plan = webserv::ListenerPlan::build(config);
webserv::Server server(plan.endpoints());
server.run();
} catch (const std::exception& error) {
webserv::log::error("{}", error.what());
-14
View File
@@ -24,16 +24,6 @@ namespace webserv {
namespace {
// Writable tail still has space -> keep using it
//
// Writable tail is exhausted, but more than 16 KiB are still live
// -> wait for more bytes to be consumed because moving them is not worth it
//
// Writable tail is exhausted and at most 16 KiB are still live
// -> move the live bytes to the start and recover the writable tail
//
// Before: [ 48 KiB consumed ][ 16 KiB live ]
// After: [ 16 KiB live ][ 48 KiB writable tail ]
void compactIfDrained(ByteBuffer& buffer) {
if (buffer.writableTail().empty() &&
buffer.readable().size() <= Connection::LOW_WATERMARK) {
@@ -45,10 +35,6 @@ void compactIfDrained(ByteBuffer& buffer) {
Connection::Connection(FileDescriptor socket) : socket_(std::move(socket)) {}
// Client can still send data -> Alive
// Read side closed, buffered data remains -> Alive
// Read side closed, both buffers empty -> Done
// recv() or send() failed -> Fatal
Connection::Outcome Connection::onClientEvent(std::uint32_t events) {
// 1) Interpret the readiness reported by epoll.
// EPOLLIN says recv() may progress.
+74
View File
@@ -0,0 +1,74 @@
#include "net/ListenerPlan.hpp"
#include <arpa/inet.h>
#include <netinet/in.h>
#include <algorithm>
#include <cstdint>
#include <cstring>
#include <format>
namespace webserv {
namespace {
// The parser only ever produces dotted IPv4 strings ("127.0.0.1", or
// "0.0.0.0" when the config omits a host), so IPv4 is all that needs
// resolving here.
sockaddr_storage makeIPv4Endpoint(const std::string& host, int port) {
sockaddr_in address{};
address.sin_family = AF_INET;
address.sin_port = ::htons(static_cast<std::uint16_t>(port));
if (::inet_pton(AF_INET, host.c_str(), &address.sin_addr) != 1) {
throw ListenerPlan::Error(std::format("invalid listen address '{}'", host));
}
sockaddr_storage storage{};
std::memcpy(&storage, &address, sizeof(address));
return storage;
}
} // namespace
ListenerPlan::Error::Error(std::string message) : msg_(std::move(message)) {}
const char* ListenerPlan::Error::what() const noexcept { return msg_.c_str(); }
ListenerPlan ListenerPlan::build(const Config& config) {
ListenerPlan plan;
// Local key list mirroring plan.endpoints_/routes_ by index, used only to
// find which group an already-seen host:port belongs to.
std::vector<ServerConfig::Listen> keys;
for (const ServerConfig& server : config.getServers()) {
for (const ServerConfig::Listen& listen : server.getListens()) {
std::size_t index = keys.size();
bool found = false;
for (std::size_t i = 0; i < keys.size(); ++i) {
if (keys[i].host == listen.host && keys[i].port == listen.port) {
index = i;
found = true;
break;
}
}
if (!found) {
keys.push_back(listen);
plan.endpoints_.push_back(makeIPv4Endpoint(listen.host, listen.port));
plan.routes_.emplace_back();
}
// A server block that repeats the same "listen" twice must not be
// added to its own route group twice.
std::vector<const ServerConfig*>& route = plan.routes_[index];
if (std::ranges::find(route, &server) == route.end()) {
route.push_back(&server);
}
}
}
return plan;
}
} // namespace webserv