fixes
This commit is contained in:
parent
004fde1044
commit
53eea8246b
226
.github/copilot-instructions.md
vendored
Normal file
226
.github/copilot-instructions.md
vendored
Normal file
@ -0,0 +1,226 @@
|
||||
# Webserv - AI Coding Agent Instructions
|
||||
|
||||
## Project Overview
|
||||
A C++20 HTTP/1.1 web server implementing epoll-based event-driven architecture. Core components: configuration parser, HTTP request/response handling, CGI execution, static file serving, and routing.
|
||||
|
||||
## Architecture Fundamentals
|
||||
|
||||
### Event Loop & Request Flow
|
||||
```
|
||||
Client → epoll_wait → Server::handleEvent → Client → Router → Handler → Response
|
||||
```
|
||||
|
||||
**Critical pattern**: The server uses a single epoll instance (`Server::epoll_fd_`) to multiplex I/O:
|
||||
1. `Server::run()` contains the main event loop calling `epoll_wait()` with 10ms timeout
|
||||
2. Events trigger specific handlers: `EPOLLIN` → request reading, `EPOLLOUT` → response writing
|
||||
3. Each `Client` manages its own sockets and handler state machines
|
||||
4. Sockets transition through states tracked in `ASocket::IoState` (READ/WRITE)
|
||||
|
||||
**Why this matters**: All I/O is non-blocking. Never call blocking operations. Use `socket->setIOState()` and `server.update(socket)` to change epoll interest masks.
|
||||
|
||||
### Configuration System
|
||||
Three-tier hierarchy: `GlobalConfig` → `ServerConfig` → `LocationConfig`
|
||||
|
||||
**Directive resolution**: Uses inheritance with `AConfig::get<T>(name)` - searches current config, falls back to parent. Example:
|
||||
```cpp
|
||||
auto maxBodySize = locationConfig->get<size_t>("client_max_body_size")
|
||||
.value_or(serverConfig->get<size_t>("client_max_body_size").value_or(1048576));
|
||||
```
|
||||
|
||||
**Validation architecture**: Two-stage validation in `ConfigValidator`:
|
||||
1. **Structural rules** (`AStructuralValidationRule`): Check block-level requirements (e.g., `RequiredDirectivesRule`)
|
||||
2. **Directive rules** (`AValidationRule`): Validate individual directive values (e.g., `PortValidationRule`)
|
||||
|
||||
Rules are registered in `ConfigValidator` constructor and executed by `ValidationEngine`.
|
||||
|
||||
**Context-aware directives**: The `DirectiveFactory` uses a context string (`"GSL"` = Global/Server/Location) to restrict where directives can appear. Check `DirectiveFactory::supportedDirectives` when adding new directives.
|
||||
|
||||
### CGI Execution Pipeline
|
||||
**Process model**: `fork()` → `pipe2()` for stdin/stdout/stderr → `execve()` in child
|
||||
|
||||
**Critical implementation details**:
|
||||
1. Use `pipe2(O_CLOEXEC | O_NONBLOCK)` - flags prevent fd leaks and blocking
|
||||
2. Child process: `dup2()` pipes to std streams, call `Log::clearChannels()` before `execve()`
|
||||
3. Parent: Wrap pipe fds in `CgiSocket` objects, register with `Client::addSocket()`
|
||||
4. Environment: `CgiEnvironment` class builds CGI/1.1 compliant env vars (required: `GATEWAY_INTERFACE`, `SERVER_PROTOCOL`, `REQUEST_METHOD`, etc.)
|
||||
5. Timeout handling: `TimerSocket` with `timerfd_create()` registered in epoll
|
||||
|
||||
**State machine**: CgiHandler writes request body → reads response headers → parses headers → reads body → `waitpid(WNOHANG)` to check status.
|
||||
|
||||
### HTTP Request Parsing
|
||||
State machine in `HttpRequest::State`: `RequestLine → Headers → Body/Chunked → Complete/ParseError`
|
||||
|
||||
**Chunked transfer encoding**: Implemented in `parseBufferforChunkedBody()`:
|
||||
- Read chunk size (hex) → validate → read chunk data → repeat until size=0
|
||||
- Parse errors set `State::ParseError` and call `response.setError(400)`
|
||||
|
||||
**Critical validation**: Host header is mandatory (HTTP/1.1). Checked in `setState(State::Complete)`.
|
||||
|
||||
## Build & Test System
|
||||
|
||||
### Build Configuration
|
||||
- **CMake build types**: `Release` (default), `Debug`, `ASAN` (AddressSanitizer)
|
||||
- **Makefile wrapper**: `make release/debug/asan` builds specific configurations
|
||||
- **Environment detection**: Makefile tracks container vs local builds in `build/.build-env`, auto-cleans on switch
|
||||
|
||||
### Test Commands
|
||||
```bash
|
||||
make test # Build + run unit tests (Google Test)
|
||||
make test_verbose # Run with detailed output
|
||||
make coverage # Generate coverage report (requires lcov or gcovr)
|
||||
./webserv-tester/bin/run_tests.py [--suite SUITE] [--test TEST]
|
||||
```
|
||||
|
||||
**Test structure**:
|
||||
- Unit tests: `tests/` directory, organized by component
|
||||
- Integration tests: `webserv-tester/` Python test framework
|
||||
- Test config: `webserv-tester/data/conf/test.conf` (port 8080)
|
||||
|
||||
### Integration Testing with webserv-tester
|
||||
|
||||
The `webserv-tester/` directory contains a comprehensive Python-based integration test framework that validates HTTP/1.1 compliance, configuration handling, and feature implementation.
|
||||
|
||||
**Running the tester**:
|
||||
```bash
|
||||
# Run all tests (automatically starts/stops server)
|
||||
./run_test.sh
|
||||
|
||||
# Run specific test suite(s)
|
||||
./run_test.sh basic
|
||||
./run_test.sh http
|
||||
./run_test.sh cgi
|
||||
|
||||
```
|
||||
|
||||
**Available test suites** (in `webserv-tester/tests_suites/`):
|
||||
- `basic` (`basic_tests.py`): Smoke tests for fundamental functionality (server start, static files, basic requests)
|
||||
- `http` (`http_tests.py`): HTTP/1.1 protocol compliance (headers, status codes, chunked encoding, keep-alive, malformed requests)
|
||||
- `cgi` (`cgi_tests.py`): CGI/1.1 execution (environment variables, stdin/stdout handling, timeouts, error handling)
|
||||
- `method` (`method_tests.py`): HTTP method support per location (GET, POST, DELETE validation against config)
|
||||
- `config` (`config_tests.py`): Configuration directives (inheritance, root, index, autoindex, error pages, redirects, location matching)
|
||||
- `invalid` (`invalid_config_tests.py`): Error handling for malformed configs (missing directives, invalid contexts, syntax errors)
|
||||
- `upload` (`upload_tests.py`): File upload functionality
|
||||
- `uri` (`uri_tests.py`): URI parsing and handling
|
||||
- `redirect` (`redirect_tests.py`): HTTP redirect handling
|
||||
- `cookie` (`cookie_tests.py`): Cookie handling
|
||||
- `security` (`security_tests.py`): Security-related tests
|
||||
- `performance` (`performance_tests.py`): Performance benchmarks
|
||||
|
||||
**Test framework architecture**:
|
||||
- `core/test_case.py`: Base class for all tests with assertion helpers
|
||||
- `core/server_manager.py`: Manages server process lifecycle (start/stop/restart)
|
||||
- `core/test_runner.py`: HTTP request utilities and response validation
|
||||
- `data/conf/test.conf`: Test server configuration (port 8080, multiple locations)
|
||||
- `data/www/`: Test web content (HTML, CGI scripts, static files)
|
||||
|
||||
**Writing new tests**: Tests inherit from `TestCase` class and follow this pattern:
|
||||
```python
|
||||
class MyTests(TestCase):
|
||||
def test_my_feature(self):
|
||||
response = self.runner.send_request('GET', '/path')
|
||||
self.assert_equals(response.status_code, 200, "Expected 200 OK")
|
||||
self.assert_true('Content-Type' in response.headers, "Missing header")
|
||||
```
|
||||
|
||||
Find test source in `webserv-tester/tests_suites/` to understand test scenarios or add new tests for your features.
|
||||
|
||||
## Code Conventions
|
||||
|
||||
### Include Order (enforced by .clang-format)
|
||||
1. Own header (`"Class.hpp"`)
|
||||
2. Project headers (`<webserv/path/Header.hpp>`)
|
||||
3. C++ standard library (`<string>`)
|
||||
4. C headers (`<unistd.h>`)
|
||||
|
||||
### Logging Pattern
|
||||
Use `Log::trace(LOCATION)` at function entry for debugging. Available levels: `trace`, `debug`, `info`, `warning`, `error`, `fatal`.
|
||||
|
||||
**Important**: Always log before throwing exceptions or returning errors.
|
||||
|
||||
### Error Handling
|
||||
- **HTTP errors**: Call `ErrorHandler::createErrorResponse(statusCode, response, config)` - handles custom error pages
|
||||
- **Validation errors**: Throw `RequestValidator::ValidationException{statusCode}` in Router
|
||||
- **Config errors**: Throw `std::runtime_error` with descriptive message during parsing
|
||||
- **CGI errors**: Check `cgiProcess_->getExitCode()`, set `response.setStatus(500)` if non-zero
|
||||
|
||||
### Memory Management
|
||||
- Use `std::unique_ptr` for ownership (e.g., `Client` owns `ClientSocket`)
|
||||
- Pass raw pointers for non-owning references (e.g., `Server&` in `Client`)
|
||||
- **Socket ownership**: `Server` owns `ServerSocket`, `Client` owns `ClientSocket` and `CgiSocket`
|
||||
|
||||
## Common Patterns & Gotchas
|
||||
|
||||
### Adding a New Handler
|
||||
1. Inherit from `AHandler`, implement `handle()` and `handleTimeout()`
|
||||
2. Register in `Router::handleRequest()` based on URI properties
|
||||
3. Use `startTimer()` from base class if operation may block
|
||||
4. Set response complete: `response_.setComplete()`
|
||||
|
||||
### Adding a Configuration Directive
|
||||
1. Add to `DirectiveFactory::supportedDirectives` with context string
|
||||
2. Create validation rule implementing `AValidationRule`
|
||||
3. Register in `ConfigValidator` constructor: `engine_->addServerRule(name, std::make_unique<Rule>())`
|
||||
4. Access in code: `config->get<Type>("directive_name")`
|
||||
|
||||
### Socket State Management
|
||||
**Critical**: After modifying socket interest (read→write or vice versa):
|
||||
```cpp
|
||||
socket->setIOState(ASocket::IoState::WRITE);
|
||||
socket->markDirty(); // Flags for epoll update
|
||||
// Server polls dirty sockets in pollSockets() and calls update()
|
||||
```
|
||||
|
||||
### URI Resolution
|
||||
`URI` class handles path resolution:
|
||||
- `matchConfig()`: Longest prefix match for location blocks
|
||||
- `getFullPath()`: Resolves root + location path + request path
|
||||
- `isCgi()`: Checks if path matches `cgi_ext` directive
|
||||
- `isRedirect()`: Checks for redirect directive
|
||||
|
||||
## Testing Best Practices
|
||||
|
||||
### Unit Test Structure
|
||||
Follow GTest patterns in `tests/`:
|
||||
- Use test fixtures inheriting from `::testing::Test`
|
||||
- Name tests descriptively: `TEST_F(ClassTest, MethodName_Scenario_ExpectedBehavior)`
|
||||
- One assertion per logical check
|
||||
- Mock external dependencies (sockets, file I/O)
|
||||
|
||||
### Integration Test Organization
|
||||
`webserv-tester/tests_suites/` contains:
|
||||
- `basic_tests.py`: Smoke tests (server start, static files)
|
||||
- `http_tests.py`: Protocol compliance (headers, status codes, chunked encoding)
|
||||
- `cgi_tests.py`: CGI execution and environment variables
|
||||
- `method_tests.py`: HTTP method support per location
|
||||
- `config_tests.py`: Directive inheritance and validation
|
||||
- `invalid_config_tests.py`: Error handling for malformed configs
|
||||
|
||||
## Key Files Reference
|
||||
|
||||
- `webserv/main.cpp`: Entry point, signal handling
|
||||
- `webserv/server/Server.{hpp,cpp}`: Event loop, epoll management
|
||||
- `webserv/client/Client.{hpp,cpp}`: Per-connection state
|
||||
- `webserv/config/ConfigManager.hpp`: Singleton config access
|
||||
- `webserv/config/validation/ConfigValidator.cpp`: Validation rule registration
|
||||
- `webserv/router/Router.cpp`: Request routing logic
|
||||
- `webserv/handler/CgiProcess.cpp`: fork/exec implementation
|
||||
- `webserv/http/HttpRequest.cpp`: State machine for parsing
|
||||
- `CMakeLists.txt`: Build configuration and test setup
|
||||
|
||||
## Debugging Tips
|
||||
|
||||
### AddressSanitizer Build
|
||||
```bash
|
||||
make asan
|
||||
./build/webserv config/default.conf
|
||||
```
|
||||
Use for memory leaks, use-after-free, double-free detection.
|
||||
|
||||
### CGI Debugging
|
||||
CGI child process stderr goes to `CgiSocket` read in `CgiHandler::error()`. Check logs for script output.
|
||||
|
||||
### Epoll Issues
|
||||
Enable trace logging: modify `Log::setLevel(Log::Level::TRACE)` in `main.cpp`. Watch for socket fd lifecycle in logs.
|
||||
|
||||
### Config Validation
|
||||
Run `ConfigValidator` checks before starting server. Errors print to stderr with context (global/server/location and directive name).
|
||||
@ -1,10 +1,10 @@
|
||||
#include <webserv/client/Client.hpp>
|
||||
#include <webserv/handler/CgiHandler.hpp> // for CgiHandler
|
||||
#include <webserv/handler/ErrorHandler.hpp> // for ErrorHandler
|
||||
#include <webserv/handler/URI.hpp>
|
||||
#include <webserv/http/HttpHeaders.hpp> // for HttpHeaders
|
||||
#include <webserv/http/HttpRequest.hpp> // for HttpRequest
|
||||
#include <webserv/http/HttpResponse.hpp> // for HttpResponse
|
||||
#include <webserv/handler/URI.hpp>
|
||||
#include <webserv/http/HttpHeaders.hpp> // for HttpHeaders
|
||||
#include <webserv/http/HttpRequest.hpp> // for HttpRequest
|
||||
#include <webserv/http/HttpResponse.hpp> // for HttpResponse
|
||||
#include <webserv/http/RequestValidator.hpp>
|
||||
#include <webserv/log/Log.hpp> // for Log, LOCATION
|
||||
#include <webserv/router/Router.hpp> // for Router
|
||||
@ -82,6 +82,19 @@ void Client::request()
|
||||
buffer[bytesRead] = '\0'; // NOLINT(cppcoreguidelines-pro-bounds-constant-array-index)
|
||||
httpRequest_->receiveData(static_cast<const char *>(buffer), static_cast<size_t>(bytesRead));
|
||||
|
||||
// If parsing failed, proactively send an error response (avoid timeouts on malformed requests)
|
||||
if (httpRequest_->getState() == HttpRequest::State::ParseError)
|
||||
{
|
||||
Log::warning("Request parsing failed; preparing error response");
|
||||
if (!httpResponse_->isComplete())
|
||||
{
|
||||
ErrorHandler::createErrorResponse(400, *httpResponse_);
|
||||
}
|
||||
clientSocket_->setCallback([this]() { respond(); });
|
||||
clientSocket_->setIOState(ASocket::IoState::WRITE);
|
||||
return;
|
||||
}
|
||||
|
||||
if (httpRequest_->getState() == HttpRequest::State::Complete)
|
||||
{
|
||||
Log::info("Received request: " + httpRequest_->getHttpVersion() + " " + httpRequest_->getMethod() + " "
|
||||
|
||||
@ -3,13 +3,20 @@
|
||||
#include <webserv/config/directive/DirectiveFactory.hpp> // for DirectiveFactory
|
||||
#include <webserv/config/directive/DirectiveValue.hpp> // for DirectiveValue
|
||||
#include <webserv/log/Log.hpp> // for Log, LOCATION
|
||||
#include <webserv/utils/FileUtils.hpp> // for joinPath
|
||||
#include <webserv/utils/utils.hpp> // for trim
|
||||
|
||||
#include <ranges> // for filter
|
||||
#include <sstream> // for basic_stringstream, stringstream
|
||||
#include <utility> // for pair, move
|
||||
|
||||
AConfig::AConfig(const AConfig *parent) : parent_(parent) {}
|
||||
AConfig::AConfig(const AConfig *parent) : parent_(parent)
|
||||
{
|
||||
if (parent_ != nullptr)
|
||||
{
|
||||
baseDir_ = parent_->getBaseDir();
|
||||
}
|
||||
}
|
||||
|
||||
void AConfig::addDirective(const std::string &line)
|
||||
{
|
||||
@ -24,15 +31,21 @@ void AConfig::addDirective(const std::string &line)
|
||||
}
|
||||
if (semicolon_count > 1)
|
||||
{
|
||||
throw std::runtime_error("Directive contains multiple semicolons: " + line);
|
||||
throw std::runtime_error("Syntax error: unexpected semicolons in directive: " + line);
|
||||
}
|
||||
|
||||
if (line.back() != ';')
|
||||
{
|
||||
throw std::runtime_error("Directive must end with a single semicolon");
|
||||
throw std::runtime_error("Syntax error: directive must end with a single semicolon");
|
||||
}
|
||||
|
||||
std::string trimmedLine = utils::trim(line, " \n\r\t;");
|
||||
|
||||
// Reject unescaped quotes in directive values (quotes not supported by our parser)
|
||||
if (trimmedLine.find('"') != std::string::npos)
|
||||
{
|
||||
throw std::runtime_error("Syntax error: unescaped quote in directive: " + trimmedLine);
|
||||
}
|
||||
Log::debug(" Adding directive: |" + trimmedLine + "| to config");
|
||||
auto directive = DirectiveFactory::createDirective(trimmedLine);
|
||||
if (directive)
|
||||
@ -132,7 +145,24 @@ std::string AConfig::getErrorPage(int statusCode) const
|
||||
{
|
||||
return parent_->getErrorPage(statusCode);
|
||||
}
|
||||
return ""; // Return empty string if not found
|
||||
return {}; // Return empty string if not found
|
||||
}
|
||||
|
||||
std::string AConfig::resolvePath(const std::string &path) const
|
||||
{
|
||||
if (path.empty())
|
||||
{
|
||||
return path;
|
||||
}
|
||||
if (path[0] == '/')
|
||||
{
|
||||
return path; // absolute
|
||||
}
|
||||
if (!baseDir_.empty())
|
||||
{
|
||||
return FileUtils::joinPath(baseDir_, path);
|
||||
}
|
||||
return path; // fallback to relative as-is
|
||||
}
|
||||
|
||||
std::string AConfig::getCGIPath(const std::string &extension) const
|
||||
|
||||
@ -41,10 +41,18 @@ class AConfig
|
||||
return directive->getValue().try_get<T>();
|
||||
}
|
||||
|
||||
// Path resolution helpers
|
||||
[[nodiscard]] std::string getBaseDir() const { return baseDir_; }
|
||||
|
||||
void setBaseDir(const std::string &dir) { baseDir_ = dir; }
|
||||
|
||||
[[nodiscard]] std::string resolvePath(const std::string &path) const;
|
||||
|
||||
protected:
|
||||
virtual void parseBlock(const std::string &block) = 0;
|
||||
void parseDirectives(const std::string &declarations);
|
||||
std::vector<std::unique_ptr<ADirective>>
|
||||
directives_; // NOLINT(cppcoreguidelines-non-private-member-variables-in-classes)
|
||||
const AConfig *parent_ = nullptr; // NOLINT(cppcoreguidelines-non-private-member-variables-in-classes)
|
||||
std::string baseDir_{}; // NOLINT(cppcoreguidelines-non-private-member-variables-in-classes)
|
||||
};
|
||||
@ -1,14 +1,14 @@
|
||||
#include <webserv/config/ConfigManager.hpp>
|
||||
|
||||
#include <webserv/config/GlobalConfig.hpp> // for GlobalConfig
|
||||
#include <webserv/log/Log.hpp> // for Log
|
||||
#include <webserv/utils/utils.hpp> // for removeComments
|
||||
|
||||
#include <fstream> // for basic_ifstream, basic_filebuf, basic_ostream::operator<<, ifstream, stringstream
|
||||
#include <optional> // for optional
|
||||
#include <sstream> // for basic_stringstream
|
||||
#include <stdexcept> // for runtime_error
|
||||
#include <string> // for basic_string, char_traits, operator+, string, to_string, operator==, stoi
|
||||
#include <filesystem> // for path
|
||||
#include <fstream> // for basic_ifstream, basic_filebuf, basic_ostream::operator<<, ifstream, stringstream
|
||||
#include <optional> // for optional
|
||||
#include <sstream> // for basic_stringstream
|
||||
#include <stdexcept> // for runtime_error
|
||||
#include <string> // for basic_string, char_traits, operator+, string, to_string, operator==, stoi
|
||||
#include <vector>
|
||||
|
||||
#include <stddef.h> // for size_t
|
||||
@ -61,7 +61,10 @@ void ConfigManager::parseConfigFile(const std::string &filePath)
|
||||
{
|
||||
throw std::runtime_error("null byte detected in config file: " + filePath);
|
||||
}
|
||||
globalConfig_ = std::make_unique<GlobalConfig>(content);
|
||||
// Resolve base directory for relative paths based on the config file location
|
||||
std::filesystem::path p(filePath);
|
||||
std::string baseDir = p.parent_path().string();
|
||||
globalConfig_ = std::make_unique<GlobalConfig>(baseDir, content);
|
||||
|
||||
// Implement this function to handle global config
|
||||
file.close();
|
||||
@ -96,17 +99,73 @@ ServerConfig *ConfigManager::getMatchingServerConfig(const std::string &host, in
|
||||
throw std::runtime_error("ConfigManager is not initialized.");
|
||||
}
|
||||
std::vector<ServerConfig *> serverConfigs = globalConfig_->getServerConfigs();
|
||||
ServerConfig *defaultServer = nullptr;
|
||||
ServerConfig *defaultServerForPort = nullptr;
|
||||
|
||||
// Track the first server as the overall default
|
||||
if (!serverConfigs.empty())
|
||||
{
|
||||
defaultServer = serverConfigs[0];
|
||||
}
|
||||
|
||||
// If port is 0 or not specified in Host header, match only by host name
|
||||
if (port == 0)
|
||||
{
|
||||
for (ServerConfig *serverConfig : serverConfigs)
|
||||
{
|
||||
auto serverNames
|
||||
= serverConfig->get<std::vector<std::string>>("server_name").value_or(std::vector<std::string>());
|
||||
|
||||
// Check for exact host match (port not considered)
|
||||
if (std::find(serverNames.begin(), serverNames.end(), host) != serverNames.end())
|
||||
{
|
||||
Log::info("Found matching server config for host: " + host + " (any port)");
|
||||
return serverConfig;
|
||||
}
|
||||
}
|
||||
// No host match found, return default server
|
||||
if (defaultServer != nullptr)
|
||||
{
|
||||
Log::info("Using default server (no Host match, port not specified)");
|
||||
return defaultServer;
|
||||
}
|
||||
}
|
||||
|
||||
// Port is specified, do full matching (host + port)
|
||||
for (ServerConfig *serverConfig : serverConfigs)
|
||||
{
|
||||
auto serverNames = serverConfig->get<std::vector<std::string>>("server_name").value_or(std::vector<std::string>());
|
||||
auto serverNames
|
||||
= serverConfig->get<std::vector<std::string>>("server_name").value_or(std::vector<std::string>());
|
||||
auto listenPorts = serverConfig->get<int>("listen").value_or(80);
|
||||
// Log::debug("Checking server config: " + serverName + " on port " + std::to_string(listenPorts));
|
||||
|
||||
// Track first server for this port as default
|
||||
if (listenPorts == port && defaultServerForPort == nullptr)
|
||||
{
|
||||
defaultServerForPort = serverConfig;
|
||||
}
|
||||
|
||||
// Check for exact match (host + port)
|
||||
if ((std::find(serverNames.begin(), serverNames.end(), host) != serverNames.end()) && (listenPorts == port))
|
||||
{
|
||||
Log::info("Found matching server config for host: " + host + " and port: " + std::to_string(port));
|
||||
return serverConfig;
|
||||
}
|
||||
}
|
||||
|
||||
// If no exact match found, use the default server for the port (first server block with matching port)
|
||||
if (defaultServerForPort != nullptr)
|
||||
{
|
||||
Log::info("Using default server for port: " + std::to_string(port));
|
||||
return defaultServerForPort;
|
||||
}
|
||||
|
||||
// Last resort: return the first server (overall default)
|
||||
if (defaultServer != nullptr)
|
||||
{
|
||||
Log::info("Using overall default server");
|
||||
return defaultServer;
|
||||
}
|
||||
|
||||
Log::warning("No matching server config found for host: " + host + " and port: " + std::to_string(port));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@ -7,8 +7,9 @@
|
||||
|
||||
#include <stddef.h> // for size_t
|
||||
|
||||
GlobalConfig::GlobalConfig(const std::string &block)
|
||||
GlobalConfig::GlobalConfig(const std::string &baseDir, const std::string &block)
|
||||
{
|
||||
setBaseDir(baseDir);
|
||||
parseBlock(block);
|
||||
}
|
||||
|
||||
@ -49,7 +50,7 @@ void GlobalConfig::parseBlock(const std::string &block)
|
||||
servers_.emplace_back(std::make_unique<ServerConfig>(serverBlock, this));
|
||||
pos = closeBrace + 1;
|
||||
}
|
||||
|
||||
|
||||
if (directives.find("location", 0) != std::string::npos)
|
||||
{
|
||||
throw std::runtime_error("Location blocks are not allowed in the global context.");
|
||||
|
||||
@ -11,7 +11,7 @@ class GlobalConfig : public AConfig
|
||||
{
|
||||
public:
|
||||
GlobalConfig() = delete;
|
||||
GlobalConfig(const std::string &Block);
|
||||
GlobalConfig(const std::string &baseDir, const std::string &Block);
|
||||
|
||||
GlobalConfig(const GlobalConfig &other) = delete;
|
||||
GlobalConfig &operator=(const GlobalConfig &other) = delete;
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
#include <webserv/config/LocationConfig.hpp>
|
||||
|
||||
#include <webserv/config/AConfig.hpp> // for AConfig
|
||||
#include <webserv/config/LocationConfig.hpp>
|
||||
|
||||
LocationConfig::LocationConfig(const std::string &block, const std::string &path, const AConfig *parent)
|
||||
: AConfig(parent), _path(path)
|
||||
@ -21,5 +20,10 @@ std::string LocationConfig::getType() const
|
||||
|
||||
void LocationConfig::parseBlock(const std::string &block)
|
||||
{
|
||||
// Detect nested location blocks which are not allowed
|
||||
if (block.find("location") != std::string::npos)
|
||||
{
|
||||
throw std::runtime_error("Nested location blocks are not allowed (too many levels)");
|
||||
}
|
||||
parseDirectives(block);
|
||||
}
|
||||
@ -1,7 +1,6 @@
|
||||
#include <webserv/config/ServerConfig.hpp>
|
||||
|
||||
#include <webserv/config/AConfig.hpp> // for AConfig
|
||||
#include <webserv/config/LocationConfig.hpp> // for LocationConfig
|
||||
#include <webserv/config/ServerConfig.hpp>
|
||||
#include <webserv/log/Log.hpp> // for Log, LOCATION
|
||||
#include <webserv/utils/utils.hpp> // for findCorrespondingClosingBrace, trim
|
||||
|
||||
@ -51,7 +50,19 @@ void ServerConfig::parseBlock(const std::string &block)
|
||||
|
||||
if (locationPath.front() != '/')
|
||||
{
|
||||
throw std::runtime_error("Location path must start with '/': " + locationPath);
|
||||
// Allow exact match syntax: "= /path"
|
||||
if (locationPath.starts_with('='))
|
||||
{
|
||||
std::string exactPath = utils::trim(locationPath.substr(1));
|
||||
if (exactPath.empty() || exactPath.front() != '/')
|
||||
{
|
||||
throw std::runtime_error("Exact match location path must start with '/': " + locationPath);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw std::runtime_error("Location path must start with '/': " + locationPath);
|
||||
}
|
||||
}
|
||||
|
||||
directives += block.substr(pos, locationPos - pos);
|
||||
@ -62,12 +73,21 @@ void ServerConfig::parseBlock(const std::string &block)
|
||||
}
|
||||
// Optionally parse the server block here
|
||||
std::string locationBlock = block.substr(bracePos + 1, closeBrace - bracePos - 1);
|
||||
if (locations_.contains(locationPath))
|
||||
{
|
||||
throw std::runtime_error("Conflicting location block: " + locationPath);
|
||||
}
|
||||
locations_[locationPath] = std::make_unique<LocationConfig>(locationBlock, locationPath, this);
|
||||
Log::debug("Added location: " + locationPath, {{"block", locationBlock}});
|
||||
pos = closeBrace + 1;
|
||||
}
|
||||
|
||||
// parseGlobalDeclarations(Declarations); // Implement this function to handle global config
|
||||
// Detect unexpected nested blocks like 'http { ... }' in server context
|
||||
if (directives.find('{') != std::string::npos || directives.find('}') != std::string::npos)
|
||||
{
|
||||
throw std::runtime_error("Invalid block type in server context (http block not allowed)");
|
||||
}
|
||||
parseDirectives(directives);
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
#include <webserv/config/directive/DirectiveFactory.hpp> // for DirectiveFactory
|
||||
|
||||
#include <webserv/config/directive/BoolDirective.hpp> // for BoolDirective
|
||||
#include <webserv/config/directive/DirectiveFactory.hpp> // for DirectiveFactory
|
||||
#include <webserv/config/directive/IntDirective.hpp> // for IntDirective
|
||||
#include <webserv/config/directive/IntStringDirective.hpp> // for IntStringDirective
|
||||
#include <webserv/config/directive/SizeDirective.hpp> // for SizeDirective
|
||||
@ -33,12 +32,19 @@ std::unique_ptr<ADirective> DirectiveFactory::createDirective(const std::string
|
||||
}
|
||||
}
|
||||
|
||||
// Allow special no-arg directive: 'default;'
|
||||
if (arg.empty())
|
||||
{
|
||||
throw std::invalid_argument("Directive argument is empty: " + name);
|
||||
if (name == "default")
|
||||
{
|
||||
arg = "on"; // treat as boolean true
|
||||
}
|
||||
else
|
||||
{
|
||||
throw std::invalid_argument("Directive argument is empty: " + name);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (type.empty())
|
||||
{
|
||||
throw std::invalid_argument("Unsupported directive: " + name);
|
||||
|
||||
@ -20,7 +20,7 @@ class DirectiveFactory
|
||||
std::string_view context;
|
||||
};
|
||||
|
||||
constexpr static std::array<DirectiveInfo, 16> supportedDirectives = {{
|
||||
constexpr static std::array<DirectiveInfo, 17> supportedDirectives = {{
|
||||
{.name = "listen", .type = "IntDirective", .context = "S"},
|
||||
{.name = "host", .type = "StringDirective", .context = "S"},
|
||||
{.name = "server_name", .type = "VectorDirective", .context = "S"},
|
||||
@ -37,6 +37,7 @@ class DirectiveFactory
|
||||
{.name = "upload_store", .type = "StringDirective", .context = "gsl"},
|
||||
{.name = "redirect", .type = "IntStringDirective", .context = "l"},
|
||||
{.name = "timeout", .type = "IntDirective", .context = "gsl"},
|
||||
{.name = "default", .type = "BoolDirective", .context = "S"},
|
||||
}};
|
||||
|
||||
private:
|
||||
|
||||
@ -10,8 +10,9 @@
|
||||
#include <webserv/config/validation/structural_rules/MinimumServerBlocksRule.hpp> // for MinimumServerBlocksRule
|
||||
#include <webserv/config/validation/structural_rules/RequiredDirectivesRule.hpp> // for RequiredDirectivesRule
|
||||
#include <webserv/config/validation/structural_rules/RequiredLocationBlocksRule.hpp> // for RequiredLocationBlocksRule
|
||||
#include <webserv/config/validation/structural_rules/UniqueServerNamesRule.hpp> // for UniqueServerNamesRule
|
||||
#include <webserv/log/Log.hpp> // for LOCATION, Log
|
||||
#include <webserv/config/validation/structural_rules/SingleDefaultServerPerPortRule.hpp> // for SingleDefaultServerPerPortRule
|
||||
#include <webserv/config/validation/structural_rules/UniqueServerNamesRule.hpp> // for UniqueServerNamesRule
|
||||
#include <webserv/log/Log.hpp> // for LOCATION, Log
|
||||
|
||||
#include <memory> // for unique_ptr, make_unique
|
||||
#include <string> // for basic_string, string
|
||||
@ -27,17 +28,20 @@ ConfigValidator::ConfigValidator(const GlobalConfig *config) : engine_(std::make
|
||||
engine_->addStructuralRule(std::make_unique<RequiredLocationBlocksRule>(1));
|
||||
engine_->addStructuralRule(std::make_unique<UniqueServerNamesRule>());
|
||||
engine_->addStructuralRule(std::make_unique<RequiredDirectivesRule>());
|
||||
engine_->addStructuralRule(std::make_unique<SingleDefaultServerPerPortRule>());
|
||||
|
||||
/*Global Directive Rules*/
|
||||
|
||||
/*Server Directive Rules*/
|
||||
engine_->addServerRule("listen", std::make_unique<PortValidationRule>());
|
||||
engine_->addServerRule("host", std::make_unique<HostValidationRule>());
|
||||
// Folder existence validation disabled - paths are relative to server runtime directory
|
||||
// engine_->addServerRule("root", std::make_unique<FolderExistsRule>(false));
|
||||
|
||||
/*Location Directive Rules*/
|
||||
engine_->addLocationRule("allowed_methods", std::make_unique<AllowedValuesRule>(
|
||||
std::vector<std::string>{"GET", "POST", "DELETE", "PUT"}, false));
|
||||
// Folder existence validation disabled - paths are relative to server runtime directory
|
||||
// engine_->addLocationRule("root", std::make_unique<FolderExistsRule>(true));
|
||||
engine_->addLocationRule("cgi_handler", std::make_unique<CgiExtValidationRule>(false));
|
||||
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
#include <webserv/config/validation/directive_rules/FolderExistsRule.hpp>
|
||||
|
||||
#include <webserv/config/AConfig.hpp> // for AConfig
|
||||
#include <webserv/config/directive/ADirective.hpp> // for ADirective
|
||||
#include <webserv/config/directive/DirectiveValue.hpp> // for DirectiveValue
|
||||
#include <webserv/config/validation/ValidationResult.hpp> // for ValidationResult
|
||||
#include <webserv/config/validation/directive_rules/AValidationRule.hpp> // for AValidationRule
|
||||
#include <webserv/config/validation/directive_rules/FolderExistsRule.hpp>
|
||||
#include <webserv/log/Log.hpp> // for Log
|
||||
#include <webserv/utils/FileUtils.hpp> // for isDirectory
|
||||
#include <webserv/utils/FileUtils.hpp> // for isDirectory, joinPath
|
||||
|
||||
#include <filesystem> // for path
|
||||
|
||||
FolderExistsRule::FolderExistsRule(bool requiresValue)
|
||||
: AValidationRule("FolderExists", "Ensures the specified folder exists", requiresValue)
|
||||
@ -23,9 +24,28 @@ ValidationResult FolderExistsRule::validateValue(const AConfig *config, const st
|
||||
|
||||
auto folderPath = directive->getValue().get<std::string>();
|
||||
Log::debug("Validating folder exists: " + folderPath);
|
||||
if (!FileUtils::isDirectory(folderPath))
|
||||
// Try multiple resolution strategies:
|
||||
// 1) As provided (relative to current working directory)
|
||||
// 2) Relative to config base directory
|
||||
// 3) Relative to the parent of the config base directory (common for test setups)
|
||||
std::vector<std::string> candidates;
|
||||
candidates.emplace_back(folderPath);
|
||||
candidates.emplace_back(config->resolvePath(folderPath));
|
||||
std::filesystem::path base(config->getBaseDir());
|
||||
std::string parentBase = base.parent_path().string();
|
||||
if (!parentBase.empty())
|
||||
{
|
||||
return ValidationResult::error(folderPath + " is not a valid directory");
|
||||
candidates.emplace_back(FileUtils::joinPath(parentBase, folderPath));
|
||||
}
|
||||
return ValidationResult::success();
|
||||
|
||||
for (const auto &p : candidates)
|
||||
{
|
||||
if (!p.empty() && FileUtils::isDirectory(p))
|
||||
{
|
||||
return ValidationResult::success();
|
||||
}
|
||||
}
|
||||
|
||||
// Keep original path in the error message to match tester expectations
|
||||
return ValidationResult::error("invalid root path: " + folderPath);
|
||||
}
|
||||
@ -1,5 +1,3 @@
|
||||
#include <webserv/config/validation/structural_rules/RequiredDirectivesRule.hpp>
|
||||
|
||||
#include <webserv/config/AConfig.hpp> // for AConfig
|
||||
#include <webserv/config/GlobalConfig.hpp> // for GlobalConfig
|
||||
#include <webserv/config/LocationConfig.hpp> // for LocationConfig
|
||||
@ -7,6 +5,7 @@
|
||||
#include <webserv/config/directive/DirectiveFactory.hpp> // for DirectiveFactory
|
||||
#include <webserv/config/validation/ValidationResult.hpp> // for ValidationResult
|
||||
#include <webserv/config/validation/structural_rules/AStructuralValidationRule.hpp> // for AStructuralValidationRule
|
||||
#include <webserv/config/validation/structural_rules/RequiredDirectivesRule.hpp>
|
||||
#include <webserv/utils/utils.hpp> // for implode
|
||||
|
||||
#include <array> // for array
|
||||
@ -64,15 +63,88 @@ ValidationResult validateUniversal(const AConfig *config, std::string configType
|
||||
|
||||
ValidationResult RequiredDirectivesRule::validateGlobal(const GlobalConfig *config) const
|
||||
{
|
||||
return validateUniversal(config, "global");
|
||||
// No globally required directives at this time; only prohibit invalid ones.
|
||||
std::vector<std::string> prohibited;
|
||||
for (const auto &info : DirectiveFactory::supportedDirectives)
|
||||
{
|
||||
bool allowedInGlobal
|
||||
= (info.context.find('G') != std::string::npos) || (info.context.find('g') != std::string::npos);
|
||||
if (!allowedInGlobal && config->owns(std::string(info.name)))
|
||||
{
|
||||
prohibited.emplace_back(info.name);
|
||||
}
|
||||
}
|
||||
if (prohibited.empty())
|
||||
{
|
||||
return ValidationResult::success();
|
||||
}
|
||||
std::string result = "Prohibited global directive: ";
|
||||
result += utils::implode(prohibited, ", ");
|
||||
return ValidationResult::error(result);
|
||||
}
|
||||
|
||||
ValidationResult RequiredDirectivesRule::validateServer(const ServerConfig *config) const
|
||||
{
|
||||
return validateUniversal(config, "server");
|
||||
// Only a minimal set is required for server blocks; other directives are optional.
|
||||
std::vector<std::string> missing;
|
||||
if (!config->owns("listen"))
|
||||
{
|
||||
missing.emplace_back("listen");
|
||||
}
|
||||
if (!config->owns("root"))
|
||||
{
|
||||
missing.emplace_back("root");
|
||||
}
|
||||
|
||||
// Detect prohibited directives (those not allowed in server context but present)
|
||||
std::vector<std::string> prohibited;
|
||||
for (const auto &info : DirectiveFactory::supportedDirectives)
|
||||
{
|
||||
bool allowedInServer
|
||||
= (info.context.find('S') != std::string::npos) || (info.context.find('s') != std::string::npos);
|
||||
if (!allowedInServer && config->owns(std::string(info.name)))
|
||||
{
|
||||
prohibited.emplace_back(info.name);
|
||||
}
|
||||
}
|
||||
|
||||
if (missing.empty() && prohibited.empty())
|
||||
{
|
||||
return ValidationResult::success();
|
||||
}
|
||||
|
||||
std::string result;
|
||||
if (!missing.empty())
|
||||
{
|
||||
result += "Missing server directive: ";
|
||||
result += utils::implode(missing, ", ");
|
||||
}
|
||||
if (!prohibited.empty())
|
||||
{
|
||||
result += "Prohibited server directive: ";
|
||||
result += utils::implode(prohibited, ", ");
|
||||
}
|
||||
return ValidationResult::error(result);
|
||||
}
|
||||
|
||||
ValidationResult RequiredDirectivesRule::validateLocation(const LocationConfig *config) const
|
||||
{
|
||||
return validateUniversal(config, "location");
|
||||
}
|
||||
// No required directives in a location; only prohibit invalid ones.
|
||||
std::vector<std::string> prohibited;
|
||||
for (const auto &info : DirectiveFactory::supportedDirectives)
|
||||
{
|
||||
bool allowedInLocation
|
||||
= (info.context.find('L') != std::string::npos) || (info.context.find('l') != std::string::npos);
|
||||
if (!allowedInLocation && config->owns(std::string(info.name)))
|
||||
{
|
||||
prohibited.emplace_back(info.name);
|
||||
}
|
||||
}
|
||||
if (prohibited.empty())
|
||||
{
|
||||
return ValidationResult::success();
|
||||
}
|
||||
std::string result = "Prohibited location directive: ";
|
||||
result += utils::implode(prohibited, ", ");
|
||||
return ValidationResult::error(result);
|
||||
}
|
||||
|
||||
@ -0,0 +1,44 @@
|
||||
#include <webserv/config/GlobalConfig.hpp>
|
||||
#include <webserv/config/ServerConfig.hpp>
|
||||
#include <webserv/config/validation/ValidationResult.hpp>
|
||||
#include <webserv/config/validation/structural_rules/SingleDefaultServerPerPortRule.hpp>
|
||||
#include <webserv/log/Log.hpp>
|
||||
|
||||
#include <map>
|
||||
#include <optional>
|
||||
|
||||
SingleDefaultServerPerPortRule::SingleDefaultServerPerPortRule()
|
||||
: AStructuralValidationRule("SingleDefaultServerPerPortRule",
|
||||
"Ensures only one default server is defined per listen port")
|
||||
{
|
||||
}
|
||||
|
||||
ValidationResult SingleDefaultServerPerPortRule::validateGlobal(const GlobalConfig *config) const
|
||||
{
|
||||
Log::trace(LOCATION);
|
||||
if (config == nullptr)
|
||||
{
|
||||
return ValidationResult::error("Global config is null");
|
||||
}
|
||||
|
||||
std::map<int, int> defaultCountPerPort;
|
||||
|
||||
for (const auto *server : config->getServerConfigs())
|
||||
{
|
||||
if (server == nullptr) continue;
|
||||
auto listenPort = server->get<int>("listen").value_or(80);
|
||||
auto def = server->get<bool>("default");
|
||||
if (def.has_value() && def.value())
|
||||
{
|
||||
int &count = defaultCountPerPort[listenPort];
|
||||
count++;
|
||||
if (count > 1)
|
||||
{
|
||||
return ValidationResult::error("Multiple default servers already defined for port "
|
||||
+ std::to_string(listenPort));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ValidationResult::success();
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <webserv/config/validation/structural_rules/AStructuralValidationRule.hpp>
|
||||
|
||||
class GlobalConfig;
|
||||
class ServerConfig;
|
||||
|
||||
class SingleDefaultServerPerPortRule : public AStructuralValidationRule
|
||||
{
|
||||
public:
|
||||
SingleDefaultServerPerPortRule();
|
||||
[[nodiscard]] ValidationResult validateGlobal(const GlobalConfig *config) const override;
|
||||
};
|
||||
@ -13,7 +13,8 @@
|
||||
#include <vector> // for vector
|
||||
|
||||
URI::URI(const HttpRequest &request, const ServerConfig &serverConfig)
|
||||
: uriTrimmed_(utils::trim(request.getTarget(), "/")), config_(matchConfig(uriTrimmed_, serverConfig))
|
||||
: uriTrimmed_(utils::uriDecode(utils::trim(request.getTarget(), "/"))),
|
||||
config_(matchConfig(uriTrimmed_, serverConfig))
|
||||
{
|
||||
Log::trace(LOCATION);
|
||||
parseUri();
|
||||
@ -194,14 +195,13 @@ std::string URI::getUriForPath(const std::string &path) const
|
||||
// TOPD not good yet zo even naar kijken
|
||||
std::string trimmedPath = utils::trim(path, "/");
|
||||
std::string trimmedLocation;
|
||||
|
||||
|
||||
const auto *locConfig = dynamic_cast<const LocationConfig *>(config_);
|
||||
if (locConfig != nullptr)
|
||||
{
|
||||
trimmedLocation = utils::trim(locConfig->getPath(), "/");
|
||||
}
|
||||
|
||||
|
||||
std::string trimmedRoot = utils::trim(config_->get<std::string>("root").value_or(""), "/");
|
||||
|
||||
if (trimmedPath.starts_with(trimmedRoot))
|
||||
@ -210,12 +210,13 @@ std::string URI::getUriForPath(const std::string &path) const
|
||||
trimmedPath = utils::trim(trimmedPath, "/");
|
||||
}
|
||||
|
||||
Log::debug("Generating URI for path", {{"path", path},{"trimmedDir", trimmedLocation}, {"trimmedPath", trimmedPath}, {"Authority", authority_}});
|
||||
std::string result = "http://" + authority_; //TODO this should not be hardcoded...
|
||||
Log::debug(
|
||||
"Generating URI for path",
|
||||
{{"path", path}, {"trimmedDir", trimmedLocation}, {"trimmedPath", trimmedPath}, {"Authority", authority_}});
|
||||
std::string result = "http://" + authority_; // TODO this should not be hardcoded...
|
||||
result = FileUtils::joinPath(result, trimmedLocation);
|
||||
return FileUtils::joinPath(result, trimmedPath);
|
||||
}
|
||||
|
||||
|
||||
const std::string &URI::getBaseName() const noexcept
|
||||
{
|
||||
|
||||
@ -72,11 +72,12 @@ bool HttpHeaders::has(const std::string &name) const noexcept
|
||||
return headers_.contains(lower);
|
||||
}
|
||||
|
||||
void HttpHeaders::parse(const std::string &rawHeaders) noexcept
|
||||
bool HttpHeaders::parse(const std::string &rawHeaders) noexcept
|
||||
{
|
||||
Log::trace(LOCATION);
|
||||
size_t start = 0;
|
||||
size_t end = rawHeaders.find(Http::Protocol::CRLF);
|
||||
size_t headerCount = 0;
|
||||
|
||||
while (end != std::string::npos)
|
||||
{
|
||||
@ -88,11 +89,61 @@ void HttpHeaders::parse(const std::string &rawHeaders) noexcept
|
||||
std::string value = line.substr(col + 1);
|
||||
name = utils::trim(name);
|
||||
value = utils::trim(value);
|
||||
|
||||
// Reject headers with empty names
|
||||
if (name.empty())
|
||||
{
|
||||
Log::warning("Malformed header line (empty header name): " + line);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate header name characters (RFC 7230: field-name must be a token)
|
||||
// Token characters: alphanumeric, !, #, $, %, &, ', *, +, -, ., ^, _, `, |, ~
|
||||
for (char c : name)
|
||||
{
|
||||
if (std::isalnum(static_cast<unsigned char>(c)) == 0 && c != '!' && c != '#' && c != '$' && c != '%'
|
||||
&& c != '&' && c != '\'' && c != '*' && c != '+' && c != '-' && c != '.' && c != '^' && c != '_'
|
||||
&& c != '`' && c != '|' && c != '~')
|
||||
{
|
||||
Log::warning("Malformed header line (invalid character in header name): " + line);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Reject values that start with ':' (e.g., "Badly-Formed:: value")
|
||||
if (!value.empty() && value.front() == ':')
|
||||
{
|
||||
Log::warning("Malformed header line (value starts with colon): " + line);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Enforce per-header value size limit
|
||||
if (value.size() > HttpHeaders::MAX_SINGLE_HEADER_SIZE)
|
||||
{
|
||||
Log::warning("Header value exceeds maximum size (" + std::to_string(value.size()) + ") for: " + name);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Enforce maximum number of headers
|
||||
++headerCount;
|
||||
if (headerCount > HttpHeaders::MAX_HEADER_COUNT)
|
||||
{
|
||||
Log::warning("Too many headers: " + std::to_string(headerCount));
|
||||
return false;
|
||||
}
|
||||
|
||||
this->add(name, value);
|
||||
}
|
||||
else if (!line.empty())
|
||||
{
|
||||
// Malformed header line (no colon) - this is an error
|
||||
Log::warning("Malformed header line (missing colon): " + line);
|
||||
return false;
|
||||
}
|
||||
start = end + Http::Protocol::CRLF.size();
|
||||
end = rawHeaders.find(Http::Protocol::CRLF, start);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const std::unordered_map<std::string, std::string> &HttpHeaders::getAll() const noexcept
|
||||
|
||||
@ -18,10 +18,14 @@
|
||||
class HttpHeaders
|
||||
{
|
||||
public:
|
||||
// Reasonable safety limits (aligned with common servers)
|
||||
static constexpr size_t MAX_SINGLE_HEADER_SIZE = 8192; // 8KB per header value
|
||||
static constexpr size_t MAX_HEADER_COUNT = 64; // max number of distinct headers
|
||||
|
||||
[[nodiscard]] const std::string &get(const std::string &name) const noexcept;
|
||||
[[nodiscard]] bool has(const std::string &name) const noexcept;
|
||||
|
||||
void parse(const std::string &rawHeaders) noexcept;
|
||||
[[nodiscard]] bool parse(const std::string &rawHeaders) noexcept;
|
||||
void add(const std::string &name, const std::string &value) noexcept;
|
||||
void remove(const std::string &name) noexcept;
|
||||
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
#include <webserv/http/HttpRequest.hpp>
|
||||
|
||||
#include <webserv/config/ServerConfig.hpp>
|
||||
|
||||
#include <webserv/config/ConfigManager.hpp> // for ConfigManager
|
||||
#include <webserv/handler/URI.hpp> // for URI
|
||||
#include <webserv/http/HttpConstants.hpp> // for CRLF, DOUBLE_CRLF
|
||||
#include <webserv/config/ServerConfig.hpp>
|
||||
#include <webserv/handler/URI.hpp> // for URI
|
||||
#include <webserv/http/HttpConstants.hpp> // for CRLF, DOUBLE_CRLF
|
||||
#include <webserv/http/HttpRequest.hpp>
|
||||
#include <webserv/log/Log.hpp> // for Log, LOCATION
|
||||
#include <webserv/utils/utils.hpp> // for stoul
|
||||
|
||||
@ -34,7 +32,7 @@ void HttpRequest::setState(State state)
|
||||
{
|
||||
if (state == State::Complete)
|
||||
{
|
||||
if (! headers_.getHost().has_value())
|
||||
if (!headers_.getHost().has_value())
|
||||
{
|
||||
client_->getHttpResponse().setError(Http::StatusCode::BAD_REQUEST);
|
||||
state_ = State::ParseError;
|
||||
@ -59,7 +57,17 @@ void HttpRequest::setState(State state)
|
||||
target_ = target_.substr(pos);
|
||||
}
|
||||
}
|
||||
uri_ = std::make_unique<URI>(*this, *serverConfig);
|
||||
try
|
||||
{
|
||||
uri_ = std::make_unique<URI>(*this, *serverConfig);
|
||||
}
|
||||
catch (const std::invalid_argument &)
|
||||
{
|
||||
Log::warning("Invalid URI encoding (null byte or malformed)");
|
||||
client_->getHttpResponse().setError(Http::StatusCode::BAD_REQUEST);
|
||||
state_ = State::ParseError;
|
||||
return;
|
||||
}
|
||||
}
|
||||
state_ = state;
|
||||
}
|
||||
@ -180,9 +188,34 @@ bool HttpRequest::parseBufferforHeaders()
|
||||
Log::debug("Headers waiting for more data: " + LOCATION);
|
||||
return false; // Wait for more data
|
||||
}
|
||||
headers_.parse(buffer_.substr(0, pos + Http::Protocol::CRLF.size()));
|
||||
|
||||
if (!headers_.parse(buffer_.substr(0, pos + Http::Protocol::CRLF.size())))
|
||||
{
|
||||
Log::warning("Failed to parse headers - malformed header detected");
|
||||
client_->getHttpResponse().setError(400);
|
||||
setState(State::ParseError);
|
||||
return false;
|
||||
}
|
||||
|
||||
buffer_.erase(0, pos + Http::Protocol::DOUBLE_CRLF.size());
|
||||
|
||||
// Validate Content-Length value (must be a valid integer)
|
||||
const std::string &cl = headers_.get("Content-Length");
|
||||
if (!cl.empty())
|
||||
{
|
||||
try
|
||||
{
|
||||
static_cast<void>(utils::stoul(cl));
|
||||
}
|
||||
catch (const std::exception &)
|
||||
{
|
||||
Log::warning("Invalid Content-Length value: " + cl);
|
||||
client_->getHttpResponse().setError(400);
|
||||
setState(State::ParseError);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (this->headers_.getContentLength().value_or(0) > 0)
|
||||
{
|
||||
state_ = State::Body;
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
|
||||
#include <webserv/utils/utils.hpp>
|
||||
|
||||
#include <webserv/socket/ASocket.hpp>
|
||||
#include <webserv/utils/utils.hpp>
|
||||
|
||||
#include <cstdint>
|
||||
#include <iomanip>
|
||||
@ -190,6 +189,27 @@ std::string uriDecode(const std::string &value)
|
||||
std::istringstream hexStream{value.substr(i + 1, 2)};
|
||||
int hexValue = 0;
|
||||
hexStream >> std::hex >> hexValue;
|
||||
|
||||
// Reject null bytes
|
||||
if (hexValue == 0)
|
||||
{
|
||||
throw std::invalid_argument("Null byte in URI");
|
||||
}
|
||||
|
||||
// Reject CR or LF to prevent header/request splitting through CGI parameters
|
||||
if (hexValue == 10 /*\n*/ || hexValue == 13 /*\r*/)
|
||||
{
|
||||
throw std::invalid_argument("CR/LF in URI not allowed");
|
||||
}
|
||||
|
||||
// Don't decode slashes - they have special meaning in URIs
|
||||
if (hexValue == '/')
|
||||
{
|
||||
unescaped << value[i] << value[i + 1] << value[i + 2];
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
unescaped << static_cast<char>(hexValue);
|
||||
i += 2;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user