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
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ serve_cov: serve.c
gcc serve.c -o serve_cov -Wall -Wextra -O2 -fprofile-arcs -ftest-coverage -lgcov

serve: serve.c
gcc serve.c -o serve -Wall -Wextra -O2 -DNDEBUG
gcc serve.c -o serve -Wall -Wextra -O2 -DNDEBUG -DRELEASE

report:
lcov --capture --directory . --output-file coverage.info
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,8 @@ This is a minimal web server designed to serve my blog. I'm writing it to be rob
- Single core (This will probably change when I get a better VPS)

# How I test it
I'm still testing manually. I usually stress test the server locally using `wrk` and see if it breaks. I also test it under `valgrind` and sanitizers.
I'm still testing manually. I usually stress test the server locally using `wrk` and see if it breaks. I also test it under `valgrind` and sanitizers.

# Known Issues
- Server replies to HTTP/1.0 clients as HTTP/1.1
- Since poll is edge triggered, when the server is full and can't accept all new connections the remaining ones are left waiting until some other event wakes up poll()
83 changes: 66 additions & 17 deletions serve.c
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,14 @@
#include <poll.h>
#include <time.h>

#ifdef RELEASE
#define PORT 80
#define LOG_DIRECTORY_SIZE_LIMIT_MB (25 * 1024)
#else
#define PORT 8080
#define LOG_DIRECTORY_SIZE_LIMIT_MB 10
#endif

#define SHOW_IO 0
#define SHOW_REQUESTS 1
#define REQUEST_TIMEOUT_SEC 5
Expand All @@ -26,7 +33,6 @@
#define LOG_BUFFER_SIZE (1<<20)
#define LOG_BUFFER_LIMIT (1<<24)
#define LOG_FLUSH_TIMEOUT_SEC 3
#define LOG_DIRECTORY_SIZE_LIMIT_MB 10
#define INPUT_BUFFER_LIMIT_MB 1

#ifndef NDEBUG
Expand Down Expand Up @@ -327,7 +333,7 @@ int parse_request_head(string str, Request *request)

char to_lower(char c)
{
if (c >= 'A' || c <= 'Z')
if (c >= 'A' && c <= 'Z')
return c - 'A' + 'a';
else
return c;
Expand All @@ -353,11 +359,13 @@ string trim(string s)
size_t cur = 0;
while (cur < s.size && is_space(s.data[cur]))
cur++;

if (cur == s.size) {
s.data = "";
s.size = 0;
} else {
s.data += cur;
s.size -= cur;
while (is_space(s.data[s.size-1]))
s.size--;
}
Expand Down Expand Up @@ -711,6 +719,7 @@ typedef struct {
ByteQueue output;
int served_count;
bool closing;
bool keep_alive;
uint64_t creation_time;
uint64_t start_time;
} Connection;
Expand Down Expand Up @@ -927,17 +936,29 @@ int num_conns = 0;

bool should_keep_alive(Connection *conn)
{
// Don't keep alive if the peer doesn't want to
if (conn->keep_alive == false) {
DEBUG("Not keeping alive because peer wants to close\n");
return false;
}

// Don't keep alive if the request is too old
if (now - conn->creation_time > CONNECTION_TIMEOUT_SEC * 1000)
if (now - conn->creation_time > CONNECTION_TIMEOUT_SEC * 1000) {
DEBUG("Not keeping alive because the connection is too old\n");
return false;
}

// Don't keep alive if we served a lot of requests to this connection
if (conn->served_count > 100)
if (conn->served_count > 100) {
DEBUG("Not keeping alive because too many requests were served using this connection\n");
return false;
}

// Don't keep alive if the server is more than 70% full
if (num_conns > 0.7 * MAX_CONNECTIONS)
if (num_conns > 0.7 * MAX_CONNECTIONS) {
DEBUG("Not keeping alive because the server is more than 70%% full\n");
return false;
}

return true;
}
Expand Down Expand Up @@ -1053,7 +1074,21 @@ bool respond_to_available_requests(struct pollfd *polldata, Connection *conn)
break; // Request wasn't completely received yet

// Reset the request timer
conns->start_time = now;
conn->start_time = now;

conn->keep_alive = false;
string keep_alive_header;
if (find_header(&request, LIT("Connection"), &keep_alive_header)) {
DEBUG("Found Connection header\n");
if (string_match_case_insensitive(trim(keep_alive_header), LIT("Keep-Alive"))) {
conn->keep_alive = true;
DEBUG("Matched Keep-Alive (%.*s)\n", (int) trim(keep_alive_header).size, trim(keep_alive_header).data);
} else {
DEBUG("Didn't match Keep-Alive (%.*s)\n", (int) trim(keep_alive_header).size, trim(keep_alive_header).data);
}
} else {
DEBUG("No Connection header\n");
}

// Respond
ResponseBuilder builder;
Expand All @@ -1069,6 +1104,11 @@ bool respond_to_available_requests(struct pollfd *polldata, Connection *conn)
polldata->events |= POLLOUT;
polldata->revents |= POLLOUT;
}
if (!conn->keep_alive) {
polldata->events &= ~POLLIN;
conn->closing = true;
conn->start_time = now;
}

pipeline_count++;
if (pipeline_count == 10) {
Expand Down Expand Up @@ -1154,6 +1194,9 @@ bool write_to_socket(int fd, ByteQueue *queue)

int main(int argc, char **argv)
{
(void) argc;
(void) argv;

signal(SIGTERM, handle_sigterm);
signal(SIGQUIT, handle_sigterm);
signal(SIGINT, handle_sigterm);
Expand Down Expand Up @@ -1277,16 +1320,22 @@ int main(int argc, char **argv)
log_data(LIT("Closing timeout\n"));
} else {
// Request timeout
byte_queue_write(&conn->output, LIT(
"HTTP/1.1 408 Request Timeout\r\n"
"Connection: Close\r\n"
"\r\n"));
conn->closing = true;
conn->start_time = now;
pollarray[i].events &= ~POLLIN;
pollarray[i].events |= POLLOUT;
pollarray[i].revents |= POLLOUT;
log_data(LIT("Request timeout\n"));
if (byte_queue_size(&conn->input) == 0) {
// Connection was idle, so just close it
remove = true;
log_data(LIT("Idle connection timeout\n"));
} else {
byte_queue_write(&conn->output, LIT(
"HTTP/1.1 408 Request Timeout\r\n"
"Connection: Close\r\n"
"\r\n"));
conn->closing = true;
conn->start_time = now;
pollarray[i].events &= ~POLLIN;
pollarray[i].events |= POLLOUT;
pollarray[i].revents |= POLLOUT;
log_data(LIT("Request timeout\n"));
}
}

} else if (!remove && (pollarray[i].revents & POLLIN)) {
Expand Down
54 changes: 44 additions & 10 deletions tests/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,24 @@

@dataclass
class Delay:
ms: int
ms: float

@dataclass
class Send:
data: bytes
timeout: Optional[int] = None
timeout: Optional[float] = None

@dataclass
class Recv:
data: bytes
timeout: Optional[int] = None
timeout: Optional[float] = None

@dataclass
class Close:
pass

def print_bytes(prefix, data):
lines = str(data).split("\r\n")
print(prefix, f"\\r\\n\n{prefix}".join(lines), sep="")
print(prefix, f"\\r\\n\n{prefix}".join(data.decode("utf-8").split("\r\n")), sep="")

def run_test(test, addr, port):

Expand All @@ -44,7 +43,7 @@ def run_test(test, addr, port):
n = conn.send(data[sent:])
if n == 0:
break
#print_bytes("< ", data[sent:sent+n])
print_bytes("< ", data[sent:sent+n])
sent += n

case Recv(data, timeout):
Expand All @@ -54,7 +53,7 @@ def run_test(test, addr, port):
chunk = conn.recv(len(data) - count)
if chunk == b"":
break
#print_bytes("> ", chunk)
print_bytes("> ", chunk)
chunks.append(chunk)
count += len(chunk)
received = b''.join(chunks)
Expand All @@ -69,31 +68,66 @@ def run_test(test, addr, port):
case _:
pass


tests = [
[
# Test "Connection: Close"
Send(b"GET / HTTP/1.1\r\nConnection: Close\r\n\r\n"),
Recv(b"HTTP/1.1 404 Not Found\r\nConnection: Close\r\n\r\n"),
Recv(b"HTTP/1.1 404 Not Found\r\nConnection: Close\r\nContent-Length: 15 \r\n\r\nNothing here :|"),
Close(),
],
[
# Test "Connection: Keep-Alive"
Send(b"GET / HTTP/1.1\r\nConnection: Keep-Alive\r\n\r\n"),
Recv(b"HTTP/1.1 404 Not Found\r\nConnection: Keep-Alive\r\nContent-Length: 15 \r\n\r\nNothing here :|"),
Send(b"GET / HTTP/1.1\r\nConnection: Close\r\n\r\n"),
Recv(b"HTTP/1.1 404 Not Found\r\nConnection: Close\r\nContent-Length: 15 \r\n\r\nNothing here :|"),
Close(),
],
[
# Test that the connection header is insensitive to case and whitespace
Send(b"GET / HTTP/1.1\r\nConnection: keEp-ALiVE \r\n\r\n"),
Recv(b"HTTP/1.1 404 Not Found\r\nConnection: Keep-Alive\r\nContent-Length: 15 \r\n\r\nNothing here :|"),
Send(b"GET / HTTP/1.1\r\nConnection: closE\r\n\r\n"),
Recv(b"HTTP/1.1 404 Not Found\r\nConnection: Close\r\nContent-Length: 15 \r\n\r\nNothing here :|"),
Close(),
],
[
Send(b"XXX\r\n\r\n"),
Recv(b"HTTP/1.1 400 Bad Request\r\nConnection: Close\r\n\r\n"),
],
[
Send(b"GET /hello HTTP/1.1\r\nConnection: Keep-Alive\r\n\r\n"),
Recv(b"HTTP/1.1 200 OK\r\nConnection: Keep-Alive\r\nContent-Length: 13 \r\n\r\nHello, world!"),
],
[
# Test request timeout
Send(b"GET /hel"),
Delay(6),
Recv(b"HTTP/1.1 408 Request Timeout\r\nConnection: Close\r\n\r\n"),
Close()
],
[
# Test idle connection timeout
Delay(6),
Close()
],
]

p = subprocess.Popen(['../serve_cov'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)

time.sleep(0.5)

total = 0
passed = 0
for i, test in enumerate(tests):
try:
run_test(test, "127.0.0.1", 8080)
print("Test", i, "passed\n")
passed += 1
except Exception as e:
print("Test", i, "failed:", e.with_traceback(None))

print("Test", i, "failed:", e.with_traceback(None), "\n")
total += 1
print("passed: ", passed, "/", total, sep="")
p.terminate()
p.wait()