Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,5 @@ webserv
# superpowers working artifacts (specs + plans live locally only)
docs/superpowers/

# unit test binary
tests/unit/run_tests
# unit test binaries (one per suite -- request, response, ...)
tests/unit/run_*_tests
33 changes: 22 additions & 11 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -53,24 +53,35 @@ clean:
fclean: clean
@echo "$(RED)🗑️ Removing executable...$(RESET)"
@rm -f $(TARGET)
@rm -f $(UNIT_BIN)
@rm -f $(UNIT_BINS)
@echo "$(GREEN)✅ Full clean complete!$(RESET)"

# Rebuild
re: fclean all

# Unit tests (phase 2.1+). Two-file build on purpose: request.cpp has no
# deps, so tests need no sockets, no server, no main.o. Same flags as the
# real build -- evaluators compile everything they find in the repo.
UNIT_SRC = tests/unit/test_request.cpp src/http/request.cpp
UNIT_BIN = tests/unit/run_tests
# Unit tests (phase 2.1+). One small two-file build per unit on purpose:
# request.cpp and response.cpp each have no deps, so a test needs no sockets,
# no server, no main.o. Same flags as the real build -- evaluators compile
# everything they find in the repo. Add a unit -> add its pair here.
REQ_SRC = tests/unit/test_request.cpp src/http/request.cpp
REQ_BIN = tests/unit/run_request_tests

unit: $(UNIT_BIN)
@./$(UNIT_BIN)
RESP_SRC = tests/unit/test_response.cpp src/http/response.cpp
RESP_BIN = tests/unit/run_response_tests

$(UNIT_BIN): $(UNIT_SRC) $(INCLUDE_DIR)/http.hpp
@echo "$(CYAN)🧪 Building unit tests...$(RESET)"
@$(CXX) $(CXXFLAGS) $(INCLUDES) -o $(UNIT_BIN) $(UNIT_SRC)
UNIT_BINS = $(REQ_BIN) $(RESP_BIN)

unit: $(UNIT_BINS)
@./$(REQ_BIN)
@./$(RESP_BIN)

$(REQ_BIN): $(REQ_SRC) $(INCLUDE_DIR)/http.hpp $(INCLUDE_DIR)/string_utils.hpp
@echo "$(CYAN)🧪 Building request unit tests...$(RESET)"
@$(CXX) $(CXXFLAGS) $(INCLUDES) -o $(REQ_BIN) $(REQ_SRC)

$(RESP_BIN): $(RESP_SRC) $(INCLUDE_DIR)/http.hpp $(INCLUDE_DIR)/string_utils.hpp
@echo "$(CYAN)🧪 Building response unit tests...$(RESET)"
@$(CXX) $(CXXFLAGS) $(INCLUDES) -o $(RESP_BIN) $(RESP_SRC)

# Show help
help:
Expand Down
66 changes: 66 additions & 0 deletions includes/http.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include <string>
#include <map>
#include <cstddef>
#include <ctime>

// Request/Response live here per the phase 0.2 layout -- Response joins in
// task 2.2.
Expand Down Expand Up @@ -87,3 +88,68 @@ class Request {
void parseHeaderLine(const std::string& line);
void setError(int code);
};

// Response is the mirror of Request: where Request decodes a byte stream
// into fields, Response builds fields into a byte stream. serialize() is the
// only thing the server loop (2.5) ever needs -- it hands the bytes to the
// write buffer.
//
// I keep it I/O-free for the same reason Request is: no sockets, no Logger.
// A handler fills in the status/headers/body and serialize() turns it into
// "HTTP/1.1 ...\r\n...\r\n\r\nbody". That also makes the unit test a
// dependency-free two-file build, like the request tests.
//
// A fresh Response is already a valid empty 200 OK -- so a GET handler can
// just setBody() and serialize() without ceremony. Content-Length and Date
// are filled in at serialize() time so they can't drift out of sync with the
// body; the caller never has to remember them.

class Response {
public:
Response();

// Status line pieces. The one-arg form looks the reason phrase up
// (setStatusCode(404) -> "Not Found") because every error site in
// 2.3/4.5 would otherwise hand-type the same strings. The two-arg
// form is the escape hatch for a code we don't name, or a CGI
// script that dictates its own reason.
void setStatusCode(int code);
void setStatusCode(int code, const std::string& reason);

// Header names are case-insensitive (RFC 7230 3.2), so I key them
// lowercased like Request does and re-canonicalize on output. That
// means setHeader("content-type", ...) and a later
// setHeader("Content-Type", ...) are the SAME header -- the second
// overwrites, no accidental duplicate.
void setHeader(const std::string& name, const std::string& value);

void setBody(const std::string& body);

// Always the real body length -- serialize() trusts this over
// anything the caller may have set, so a wrong Content-Length can't
// desync the framing (a classic smuggling bug).
std::size_t getContentLength() const;

// The whole message as one string. const because building bytes
// shouldn't mutate the object; the auto Date/Content-Length are
// merged into a local copy of the headers, not stored back.
std::string serialize() const;

// RFC 1123 date ("Sun, 06 Nov 1994 08:49:37 GMT"). Static and takes
// the time explicitly so it's testable without freezing the clock --
// serialize() feeds it time(NULL). Built by hand instead of
// strftime("%a"/"%b") because those are locale-dependent and HTTP
// dates MUST be the C locale's English abbreviations.
static std::string httpDate(std::time_t t);

// Canonical reason phrase for the codes this server actually emits,
// "" for anything we don't name. Kept as a table so the status line
// and 4.5's error pages read from one source of truth.
static std::string reasonPhrase(int code);

private:
int status_;
std::string reason_;
std::string body_;
std::map<std::string, std::string> headers_; // keys lowercased
};
20 changes: 20 additions & 0 deletions includes/string_utils.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,24 @@ std::string toString(const T& value) {
std::ostringstream oss;
oss << value;
return oss.str();
}

// ASCII-only lowercase. std::tolower is locale-dependent AND undefined
// behaviour on negative chars -- any byte >= 0x80 when char is signed, which
// network input absolutely contains. HTTP is ASCII, so I roll it by hand and
// skip <cctype> entirely. Both the request parser and the response builder
// need case-insensitive header keys, so it lives here instead of being copied
// into each. inline because this header lands in several translation units --
// a non-inline definition would break the ODR at link.
inline char asciiLower(char c) {
if (c >= 'A' && c <= 'Z')
return static_cast<char>(c + 32);
return c;
}

inline std::string lowerCopy(const std::string& s) {
std::string out(s);
for (std::size_t i = 0; i < out.size(); ++i)
out[i] = asciiLower(out[i]);
return out;
}
22 changes: 21 additions & 1 deletion includes/utils.hpp
Original file line number Diff line number Diff line change
@@ -1 +1,21 @@
#pragma once
#pragma once

#include <string>
#include <fstream>
#include <sstream>

// Slurp a whole file into `out`. Returns false if the path can't be opened,
// true otherwise. Deliberately error-policy-free: the config loader turns a
// false into a ConfigException, the token-dump tool prints and bails, and the
// 2.3 static handler will turn it into a 404/403 -- each caller owns its own
// failure story, so this stays a plain bool and never throws or logs. inline
// because the header lands in several translation units.
inline bool readFileToString(const std::string& path, std::string& out) {
std::ifstream in(path.c_str());
if (!in)
return false;
std::stringstream ss;
ss << in.rdbuf();
out = ss.str();
return true;
}
12 changes: 6 additions & 6 deletions kanban.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@

---

### [ ] 2.2: HTTP Response Construction
### [ completed ] 2.2: HTTP Response Construction
**Assignee:** Dev B
**Description:**
- Create Response class in includes/http.hpp and src/http/response.cpp
Expand All @@ -259,11 +259,11 @@
- Auto-set Content-Length and Date headers

**DoD:**
- [ ] Constructs valid HTTP response with status, headers, body
- [ ] Response line correct: "HTTP/1.1 200 OK\r\n"
- [ ] Headers properly formatted with \r\n
- [ ] Body appended after blank line
- [ ] serialize() produces valid HTTP response
- [x] Constructs valid HTTP response with status, headers, body
- [x] Response line correct: "HTTP/1.1 200 OK\r\n"
- [x] Headers properly formatted with \r\n
- [x] Body appended after blank line
- [x] serialize() produces valid HTTP response

---

Expand Down
9 changes: 3 additions & 6 deletions src/config/config.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
#include "../../includes/webserv.hpp"
#include "../../includes/parser.hpp"
#include <fstream>
#include <sstream>

// the ctors for the config structs, and nothing else (this was the first
Expand Down Expand Up @@ -59,12 +58,10 @@ const std::vector<ServerConfig>& Config::servers() const {
// ConfigException formatted "<path>:<line>: ...".

static std::string readFile(const std::string& path) {
std::ifstream in(path.c_str());
if (!in)
std::string source;
if (!readFileToString(path, source))
throw ConfigException("cannot open config file '" + path + "'");
std::stringstream ss;
ss << in.rdbuf();
return ss.str();
return source;
}

void Config::load(const std::string& path) {
Expand Down
15 changes: 6 additions & 9 deletions src/config/parser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,17 @@
#include <sstream>
#include <iostream> // std::cerr — for the unknown-directive warnings
#include <limits> // std::numeric_limits — the overflow check in parseSize
#include <fstream> // std::ifstream — reading an included file

// path helpers for the `include` directive. static so they stay local to this
// file. there's near-identical code in config.cpp — I could pull both into a
// shared util in Phase 4 polish, but right now copying a few lines is cheaper
// than the header churn that would cause.
// file. the actual file slurp is shared with config.cpp now — readFileToString
// in utils.hpp — so all that's left here is wrapping its bool failure in the
// "included file" flavour of ConfigException.

static std::string readIncludedFile(const std::string& path) {
std::ifstream in(path.c_str());
if (!in)
std::string source;
if (!readFileToString(path, source))
throw ConfigException("cannot open included file '" + path + "'");
std::stringstream ss;
ss << in.rdbuf();
return ss.str();
return source;
}

// grab the directory part of `path` — everything up to and including the last
Expand Down
18 changes: 1 addition & 17 deletions src/http/request.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "../../includes/http.hpp"
#include "../../includes/string_utils.hpp" // asciiLower / lowerCopy

// Incremental HTTP request parser -- the state machine picture and the
// no-exceptions rationale live in http.hpp. This file grows with the
Expand All @@ -17,23 +18,6 @@ static const std::size_t MAX_REQUEST_LINE = 8 * 1024;
static const std::size_t MAX_HEADER_LINE = 8 * 1024;
static const std::size_t MAX_HEADER_COUNT = 100;

// Helper: std::tolower is locale-dependent AND undefined behaviour on
// negative chars -- any byte >= 0x80 when char is signed, which network
// input absolutely contains. HTTP is ASCII, so roll it by hand and skip
// <cctype> entirely.
static char asciiLower(char c) {
if (c >= 'A' && c <= 'Z')
return static_cast<char>(c + 32);
return c;
}

static std::string lowerCopy(const std::string& s) {
std::string out(s);
for (std::size_t i = 0; i < out.size(); ++i)
out[i] = asciiLower(out[i]);
return out;
}

// OWS = "optional whitespace" in the RFC 7230 grammar: SP and HTAB only.
// Not isspace() -- that would also eat \v\f\r, which the grammar doesn't.
static std::string trimOws(const std::string& s) {
Expand Down
Loading
Loading