Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2114e11f90 | |||
| 72d072bb6c | |||
| 751b54db18 | |||
| bdb434d9d5 |
@@ -50,14 +50,29 @@ CLANG_STDLIB ?= -stdlib=libc++
|
|||||||
# find returns files in filesystem order, which is not the same on every
|
# 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
|
# machine. sort keeps the compile and link lines stable, so build logs and
|
||||||
# clang-tidy output stay comparable.
|
# clang-tidy output stay comparable.
|
||||||
SRCS := $(sort $(shell find $(SRC_DIR) -type f -name '*.cpp'))
|
#SRCS := $(sort $(shell find $(SRC_DIR) -type f -name '*.cpp'))
|
||||||
OBJS := $(SRCS:$(SRC_DIR)/%.cpp=$(OBJ_DIR)/%.o)
|
|
||||||
|
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
|
# 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
|
# 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.
|
# also depends on include/Log.hpp, and -include at the bottom pulls them in.
|
||||||
DEPS := $(OBJS:.o=.d)
|
DEPS := $(OBJS:.o=.d)
|
||||||
HDRS := $(sort $(shell find $(INC_DIR) $(SRC_DIR) -type f \
|
HDRS := include/config/Config.hpp include/config/ConfigurationParser.hpp include/config/LocationConfig.hpp include/config/ServerConfig.hpp \
|
||||||
\( -name '*.hpp' -o -name '*.tpp' \)))
|
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)
|
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@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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -29,15 +29,9 @@ class Connection final {
|
|||||||
std::uint32_t desiredEvents;
|
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;
|
static constexpr std::size_t LOW_WATERMARK = 16uz * 1024;
|
||||||
|
|
||||||
explicit Connection(FileDescriptor socket);
|
explicit Connection(FileDescriptor socket);
|
||||||
|
|
||||||
// Each Connection has a unique identity and remains in its original slot.
|
|
||||||
Connection(const Connection&) = delete;
|
Connection(const Connection&) = delete;
|
||||||
Connection& operator=(const Connection&) = delete;
|
Connection& operator=(const Connection&) = delete;
|
||||||
Connection(Connection&&) = delete;
|
Connection(Connection&&) = delete;
|
||||||
@@ -52,14 +46,8 @@ class Connection final {
|
|||||||
void close() noexcept { socket_.reset(); }
|
void close() noexcept { socket_.reset(); }
|
||||||
|
|
||||||
private:
|
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 receive();
|
||||||
bool transmit();
|
bool transmit();
|
||||||
// TEMP echo path: queues received bytes unchanged for sending back to
|
|
||||||
// the client.
|
|
||||||
void forwardReceivedBytes();
|
void forwardReceivedBytes();
|
||||||
|
|
||||||
FileDescriptor socket_;
|
FileDescriptor socket_;
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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() + "'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
@@ -10,12 +10,8 @@
|
|||||||
/* */
|
/* */
|
||||||
/* ************************************************************************** */
|
/* ************************************************************************** */
|
||||||
|
|
||||||
#include <netinet/in.h>
|
|
||||||
|
|
||||||
#include <array>
|
|
||||||
#include <cerrno>
|
#include <cerrno>
|
||||||
#include <csignal>
|
#include <csignal>
|
||||||
#include <cstdint>
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <exception>
|
#include <exception>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
@@ -24,24 +20,10 @@
|
|||||||
#include "Log.hpp"
|
#include "Log.hpp"
|
||||||
#include "Server.hpp"
|
#include "Server.hpp"
|
||||||
|
|
||||||
constexpr std::string_view DEFAULT_CONFIG_PATH = "conf/default.conf";
|
#include "config/ConfigurationParser.hpp"
|
||||||
|
#include "net/ListenerPlan.hpp"
|
||||||
|
|
||||||
namespace {
|
constexpr std::string_view DEFAULT_CONFIG_PATH = "conf/_default.conf";
|
||||||
|
|
||||||
// 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
|
|
||||||
|
|
||||||
int main(int argc, char** argv) {
|
int main(int argc, char** argv) {
|
||||||
if (argc > 2) {
|
if (argc > 2) {
|
||||||
@@ -54,6 +36,30 @@ int main(int argc, char** argv) {
|
|||||||
|
|
||||||
webserv::log::info("configuration file {}", configPath);
|
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
|
// Ignore SIGPIPE so writing to a closed socket or pipe reports an error
|
||||||
// instead of terminating the server. Other clients can then continue.
|
// instead of terminating the server. Other clients can then continue.
|
||||||
if (::signal(SIGPIPE, SIG_IGN) == SIG_ERR) {
|
if (::signal(SIGPIPE, SIG_IGN) == SIG_ERR) {
|
||||||
@@ -62,11 +68,8 @@ int main(int argc, char** argv) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// TEMP: The echo server listens on three fixed ports until main obtains
|
const webserv::ListenerPlan plan = webserv::ListenerPlan::build(config);
|
||||||
// the listener addresses from the configuration file.
|
webserv::Server server(plan.endpoints());
|
||||||
const std::array<sockaddr_storage, 3> endpoints{
|
|
||||||
anyAddress(8000), anyAddress(8001), anyAddress(8002)};
|
|
||||||
webserv::Server server(endpoints);
|
|
||||||
server.run();
|
server.run();
|
||||||
} catch (const std::exception& error) {
|
} catch (const std::exception& error) {
|
||||||
webserv::log::error("{}", error.what());
|
webserv::log::error("{}", error.what());
|
||||||
|
|||||||
@@ -24,16 +24,6 @@ namespace webserv {
|
|||||||
|
|
||||||
namespace {
|
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) {
|
void compactIfDrained(ByteBuffer& buffer) {
|
||||||
if (buffer.writableTail().empty() &&
|
if (buffer.writableTail().empty() &&
|
||||||
buffer.readable().size() <= Connection::LOW_WATERMARK) {
|
buffer.readable().size() <= Connection::LOW_WATERMARK) {
|
||||||
@@ -45,10 +35,6 @@ void compactIfDrained(ByteBuffer& buffer) {
|
|||||||
|
|
||||||
Connection::Connection(FileDescriptor socket) : socket_(std::move(socket)) {}
|
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) {
|
Connection::Outcome Connection::onClientEvent(std::uint32_t events) {
|
||||||
// 1) Interpret the readiness reported by epoll.
|
// 1) Interpret the readiness reported by epoll.
|
||||||
// EPOLLIN says recv() may progress.
|
// EPOLLIN says recv() may progress.
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user