├── .gitattributes ├── .gitignore ├── README.md ├── app └── server.c ├── codecrafters.yml └── your_server.sh /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.vscode/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HTTP Server 2 | 3 | This project is a simple HTTP server implemented in C with the help of the platform CodeCrafters. It handles basic HTTP GET and POST requests and can serve files from a specified directory. The server listens on port 4221 by default. 4 | 5 | ## Features 6 | 7 | * Serve static files from a specified directory. 8 | * Handle GET and POST requests. 9 | * Echo responses for specific endpoints. 10 | * Return User-Agent details. 11 | * Allow file uploads via POST requests. 12 | 13 | ## Setup and Usage 14 | 15 | ### Compilation 16 | 17 | Once you have cloned the repo and are in the project folder, you can compile the server with the following commands. 18 | 19 | ```bash 20 | cd app 21 | gcc server.c -o server 22 | ``` 23 | 24 | ### Running the server 25 | 26 | Run the compiled server with an optional --directory argument to specify the root directory for serving files: 27 | 28 | ```bash 29 | ./server --directory /path/to/directory 30 | ``` 31 | 32 | If no directory is specified, the server will use the current directory. 33 | 34 | ## Testing the Server 35 | 36 | You can test the server using curl. Below are some examples: 37 | 38 | 1. Simple GET Request 39 | 40 | ```bash 41 | curl -v http://localhost:4221/ 42 | ``` 43 | 44 | 2. Echo Request 45 | 46 | ```bash 47 | curl -v http://localhost:4221/echo/hello 48 | ``` 49 | 50 | 3. User-Agent Request 51 | ```bash 52 | curl -v http://localhost:4221/user-agent 53 | ``` 54 | 55 | 4. File Upload 56 | ```bash 57 | curl -v -X POST http://localhost:4221/files/upload.txt -d 'Hello World' 58 | ``` 59 | 60 | 5. File Download 61 | ```bash 62 | curl -v http://localhost:4221/files/upload.txt 63 | ``` 64 | 65 | ## How it works 66 | 67 | ### main() 68 | 69 | The main function sets up the server, including the following steps: 70 | 71 | 1. Parse command-line arguments to get the directory to serve files from. 72 | 2. Change the working directory to the specified directory. 73 | 3. Set up the server socket to listen on port 4221. 74 | 4. Use setsockopt to set SO_REUSEPORT to avoid "Address already in use" errors. 75 | 5. Bind the socket to the address and port. 76 | 6. Listen for incoming connections. 77 | 7. Accept connections and handle them in a child process using fork. 78 | 79 | ### handle_connection() 80 | 81 | The handle_connection function processes the client's request and sends an appropriate response: 82 | 83 | 1. Read the request using recv. 84 | 2. Parse the HTTP method and requested path. 85 | 3. Handle different paths: 86 | * /: Respond with "200 OK". 87 | * /echo/: Echo the rest of the path. 88 | * /user-agent: Return the User-Agent header. 89 | * /files/ with GET: Serve the requested file. 90 | * /files/ with POST: Save the uploaded content to the specified file. 91 | 4. Send the response using send. 92 | -------------------------------------------------------------------------------- /app/server.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | // Test: curl -v -X POST http://localhost:4221/files/readme.txt -d 'Fuck this shit' 11 | // Test: 12 | 13 | /** 14 | * Important things I did not know: 15 | * 16 | * 1. What is a file descriptor (fd)? 17 | * 18 | * According to Dave (ChatGPT): 19 | * "In Unix-like operating systems, including , 20 | * file descriptors are integer values that the operating 21 | * system uses to uniquely identify an open file or a 22 | * communication channel. These channels can include 23 | * regular files, sockets, pipes, character devices, and 24 | * others." 25 | * 26 | * It is essentially an identifier for operations such 27 | * as binding, listening, accepting connections, etc. 28 | * 29 | * 2. What is big-endian and little-endian?Linux 30 | * 31 | * Some processors store the least significant bytes first, 32 | * `Little-endian`, while others store the most significant bytes first, 33 | * `Big-endian`. Due to this, sometimes sending data over the network turns 34 | * into a complicated mess. To account for this `Network Byte Order` was 35 | * established. Network Byte Order is almost always `Big-endian`. 36 | * 37 | */ 38 | 39 | #define PORT 4221 40 | 41 | /** 42 | * Create a thread function 43 | */ 44 | 45 | void handle_connection(int client_fd); 46 | 47 | int main(int argc, char **argv) 48 | { 49 | char *directory = "."; 50 | if (argc >= 3) 51 | { 52 | if (strcmp(argv[1], "--directory") == 0) 53 | { 54 | directory = argv[2]; 55 | } 56 | } 57 | printf("Setting up directory to %s\n", directory); 58 | 59 | if (chdir(directory) < 0) 60 | { 61 | printf("Failed to set current dir"); 62 | return 1; 63 | } 64 | 65 | /** 66 | * Disable output buffering. 67 | * This means that the program will not wait to 68 | * output to the terminal 69 | * 70 | * For example, when doing 71 | * printf("a"); 72 | * printf("b"); 73 | * printf("c"); 74 | * The program will output a,b, and c immediately after 75 | * each print instead of waiting for a newline ('\n') 76 | */ 77 | setbuf(stdout, NULL); 78 | 79 | // printf for debugging 80 | printf("Starting program :)!\n"); 81 | 82 | /** 83 | * Creates a variable srv_addr that stores an IPv4 socket address. 84 | * .sin_family = AF_INET -> Indicates that the socket address is an IPv4 address 85 | * .sin_port = htons(PORT) -> Converts the port number from `Host Byte Order` to 86 | * `Network Byte Order` 87 | * .sin_addr = { htonl(INADDR_ANY) } -> "Sets the IP address to INADDR_ANY, which 88 | * means the socket will be bound to all available 89 | * network interfaces on the machine. INADDR_ANY is 90 | * a constant that represents "any" network interface." 91 | * - Dave (ChatGPT) 92 | * 93 | * "The htons() function converts the unsigned short integer hostshort from host byte order to network byte order. " 94 | * "The htonl() function converts the unsigned integer hostlong from host byte order to network byte order. " 95 | * - https://linux.die.net/man/3/htons 96 | * 97 | * struct sockaddr_in is a struct from in.h that has the following data: 98 | * sin_port -> Port number 99 | * sin_addr -> Internet address 100 | * sin_zero -> Padding to make the structure the same size as sockaddr 101 | */ 102 | struct sockaddr_in serv_addr = { 103 | .sin_family = AF_INET, // IPv4 104 | .sin_port = htons(PORT), // `Host Byte Order` to `Network Byte Order` short 105 | .sin_addr = {htonl(INADDR_ANY)}, // `Host Byte Order` to `Network Byte Order` long 106 | }; 107 | 108 | /** 109 | * Creates a socket connection using IPv4 (AF_INET), and TCP (SOCK_STREAM) 110 | * Other options include AF_INET6 for IPv6 and SOCK_DGRAM for UDP. 111 | * 112 | * The last parameter is the protocol, which according to the documentation: 113 | * 114 | * "If the protocol parameter is set to 0, the system selects the default 115 | * protocol number for the domain and socket type requested." 116 | * 117 | * I was a bit confused about why is it necessary to specify the protocol twice. 118 | * For instance, SOCK_STREAM is already selecting TCP, but the function also 119 | * expects IPPROTO_TCP as the third parameter. I couldn't really find an answer 120 | * so I assume it is for safety reasons or if you know what you are doing. 121 | * 122 | * socket() returns a number greater or equal than 0 if the connection was successful 123 | * or -1 if an error occurred 124 | */ 125 | int server_fd = socket(AF_INET, SOCK_STREAM, 0); // Variable to represent the file descriptor (fd) of the server socket 126 | if (server_fd == -1) 127 | { 128 | printf("Socket creation failed: %s...\n", strerror(errno)); 129 | return 1; 130 | } 131 | 132 | /** 133 | * Since the tester restarts your program quite often, setting REUSE_PORT 134 | * ensures that we don't run into 'Address already in use' errors 135 | */ 136 | int reuse = 1; 137 | if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEPORT, &reuse, sizeof(reuse)) < 0) 138 | { 139 | printf("SO_REUSEPORT failed: %s \n", strerror(errno)); 140 | return 1; 141 | } 142 | 143 | /** 144 | * According to IBM: 145 | * 146 | * "The bind() function binds a unique local name to the socket with descriptor socket. 147 | * After calling socket(), a descriptor does not have a name associated with it. 148 | * However, it does belong to a particular address family as specified when socket() 149 | * is called. The exact format of a name depends on the address family." 150 | * 151 | * - https://www.ibm.com/docs/en/zos/2.3.0?topic=functions-bind-bind-name-socket 152 | * 153 | * In other words: when you first create a socket using `socket()` function, the socket is 154 | * not yet associated with a specific address or port. 155 | * 156 | * Params: 157 | * -> server_fd is the socket identifier that we created before. 158 | * -> serv_addr is the struct that we specified. serv_addr is casted from 159 | * sockaddr_in to sockaddr since sockaddr is the generic struct for socket addresses. 160 | * -> The size of the socket address structure. 161 | * 162 | * The function should return 0 if the binding was successful 163 | */ 164 | if (bind(server_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) != 0) 165 | { 166 | printf("Bind failed: %s \n", strerror(errno)); 167 | return 1; 168 | } 169 | 170 | int connection_backlog = 5; // Maximum length of the queue of pending connections 171 | 172 | /** 173 | * `listen()` indicates that the server socket is ready to accept incoming connections 174 | * 175 | * returns 0 if connection was successful 176 | */ 177 | if (listen(server_fd, connection_backlog) != 0) 178 | { 179 | printf("Listen failed: %s \n", strerror(errno)); 180 | return 1; 181 | } 182 | 183 | while (1) 184 | { 185 | printf("Server started.\n"); 186 | printf("\tWaiting for clients to connect...\n"); 187 | 188 | struct sockaddr_in client_addr; // Variable of type struct sockaddr_in to store the client address 189 | int client_addr_len = sizeof(client_addr); // Variable to store the length of the struct client_addr 190 | 191 | int client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_addr_len); // Variable to represent the file descriptor (fd) of the client socket 192 | 193 | if (client_fd == -1) 194 | { 195 | printf("Failed to connect: %s \n", strerror(errno)); 196 | return 1; 197 | } 198 | 199 | printf("Client connected\n"); 200 | 201 | // If the current process is the child process 202 | if (!fork()) 203 | { 204 | close(server_fd); 205 | handle_connection(client_fd); 206 | close(client_fd); 207 | exit(0); 208 | } 209 | close(client_fd); 210 | } 211 | 212 | /** 213 | * \r\n\r\n represents the CRLF: 214 | * CR = Carriage Return (\r, 0x0D in hexadecimal, 13 in decimal) — moves the cursor to the beginning 215 | * of the line without advancing to the next line. 216 | * 217 | * LF = Line Feed (\n, 0x0A in hexadecimal, 10 in decimal) — moves the cursor down to the next line 218 | * without returning to the beginning of the line. 219 | * 220 | * - https://developer.mozilla.org/en-US/docs/Glossary/CRLF 221 | */ 222 | 223 | return 0; 224 | } 225 | 226 | void handle_connection(int client_fd) 227 | { 228 | // printf("Handle Connection\n"); 229 | printf("\n"); 230 | 231 | /** 232 | * `recv()` receives data on the client_fd socket and stores it in the readBuffer buffer. 233 | * If successful, returns the length of the message or datagram in bytes, otherwise 234 | * returns -1. 235 | */ 236 | 237 | char readBuffer[1024]; 238 | int bytesReceived = recv(client_fd, readBuffer, sizeof(readBuffer), 0); 239 | 240 | // printf("\n\nData: \n%s\n\n", readBuffer); 241 | 242 | if (bytesReceived == -1) 243 | { 244 | printf("Receiving failed: %s \n", strerror(errno)); 245 | // return NULL; 246 | exit(1); 247 | } 248 | 249 | char *method = strdup(readBuffer); // "GET /some/path HTTP/1.1..." 250 | char *content = strdup(readBuffer); // "GET /some/path HTTP/1.1..." 251 | printf("Content: %s\n", content); 252 | method = strtok(method, " "); // GET POST PATCH and so on 253 | printf("Method: %s\n", method); 254 | 255 | // Extract the path -> "GET /some/path HTTP/1.1..." 256 | char *reqPath = strtok(readBuffer, " "); // -> "GET" 257 | reqPath = strtok(NULL, " "); // -> "/some/path" 258 | 259 | int bytesSent; 260 | 261 | /** 262 | * `send()` sends data on the client_fd socket. 263 | * If successful, returns 0 or greater indicating the number of bytes sent, otherwise 264 | * returns -1. 265 | */ 266 | 267 | if (strcmp(reqPath, "/") == 0) 268 | { 269 | char *res = "HTTP/1.1 200 OK\r\n\r\n"; // HTTP response 270 | printf("Sending response: %s\n", res); 271 | bytesSent = send(client_fd, res, strlen(res), 0); 272 | } 273 | else if (strncmp(reqPath, "/echo/", 6) == 0) 274 | { 275 | // Parse the content 276 | reqPath = strtok(reqPath, "/"); // reqPath -> echo 277 | reqPath = strtok(NULL, ""); // reqPath -> foo/bar 278 | int contentLength = strlen(reqPath); 279 | 280 | char response[512]; 281 | sprintf(response, "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: %d\r\n\r\n%s", contentLength, reqPath); 282 | printf("Sending response: %s\n", response); 283 | bytesSent = send(client_fd, response, strlen(response), 0); 284 | } 285 | else if (strcmp(reqPath, "/user-agent") == 0) 286 | { 287 | // Parse headers 288 | reqPath = strtok(NULL, "\r\n"); // reqPath -> HTTP/1.1 289 | reqPath = strtok(NULL, "\r\n"); // reqPath -> Host: 127.0.1:4221 290 | reqPath = strtok(NULL, "\r\n"); // reqPath -> User-Agent: curl/7.81.0 291 | 292 | // Parse the body 293 | char *body = strtok(reqPath, " "); // body -> User-Agent: 294 | body = strtok(NULL, " "); // body -> curl/7.81.0 295 | int contentLength = strlen(body); 296 | 297 | char response[512]; 298 | sprintf(response, "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: %d\r\n\r\n%s", contentLength, body); 299 | printf("Sending response: %s\n", response); 300 | bytesSent = send(client_fd, response, strlen(response), 0); 301 | } 302 | else if (strncmp(reqPath, "/files/", 7) == 0 && strcmp(method, "GET") == 0) 303 | { 304 | // Parse the file path 305 | char *filename = strtok(reqPath, "/"); 306 | filename = strtok(NULL, ""); 307 | 308 | // Open the file and check if the file exists 309 | FILE *fp = fopen(filename, "rb"); 310 | if (!fp) 311 | { 312 | // If it doesn't exist, return 404 313 | printf("File not found"); 314 | char *res = "HTTP/1.1 404 Not Found\r\n\r\n"; // HTTP response 315 | bytesSent = send(client_fd, res, strlen(res), 0); 316 | } 317 | else 318 | { 319 | printf("Opening file %s\n", filename); 320 | } 321 | 322 | // Read in binary and set the cursor at the end 323 | if (fseek(fp, 0, SEEK_END) < 0) 324 | { 325 | printf("Error reading the document\n"); 326 | } 327 | 328 | // Get the size of the file 329 | size_t data_size = ftell(fp); 330 | 331 | // Rewind the cursor back 332 | rewind(fp); 333 | 334 | // Allocate enough memory for the contents 335 | void *data = malloc(data_size); 336 | 337 | // Fill in the content 338 | if (fread(data, 1, data_size, fp) != data_size) 339 | { 340 | printf("Error reading the document\n"); 341 | } 342 | 343 | fclose(fp); 344 | 345 | // Return contents 346 | char response[1024]; 347 | sprintf(response, "HTTP/1.1 200 OK\r\nContent-Type: application/octet-stream\r\nContent-Length: %ld\r\n\r\n%s", data_size, (char *)data); 348 | printf("Sending response: %s\n", response); 349 | bytesSent = send(client_fd, response, strlen(response), 0); 350 | } 351 | else if (strncmp(reqPath, "/files/", 7) == 0 && strcmp(method, "POST") == 0) 352 | { 353 | method = strtok(NULL, "\r\n"); // HTTP 1.1 354 | method = strtok(NULL, "\r\n"); // Content-Type 355 | method = strtok(NULL, "\r\n"); // User-Agent 356 | method = strtok(NULL, "\r\n"); // Accept: */* 357 | method = strtok(NULL, "\r\n"); // Content-Length: X 358 | 359 | char *contentLengthStr = strtok(method, " "); 360 | contentLengthStr = strtok(NULL, " "); 361 | 362 | int contentLength = atoi(contentLengthStr); 363 | 364 | // Parse the file path 365 | char *filename = strtok(reqPath, "/"); 366 | filename = strtok(NULL, ""); 367 | 368 | // Get the contents 369 | content = strtok(content, "\r\n"); // Content: POST /files/dumpty_yikes_dooby_237 HTTP/1.1 370 | content = strtok(NULL, "\r\n"); // Host: localhost:4221 371 | content = strtok(NULL, "\r\n"); // User-Agent: curl/7.81.0 372 | content = strtok(NULL, "\r\n"); // Accept: */* 373 | content = strtok(NULL, "\r\n"); // Content-Length: 51 374 | content = strtok(NULL, "\r\n"); // Content-Type: application/x-www-form-urlencoded 375 | content = strtok(NULL, "\r\n"); // Content-Type: application/x-www-form-urlencoded 376 | 377 | printf("\n---\nCreate a file %s with content length: %d\n\n %s\n---\n", filename, contentLength, content); 378 | 379 | // Open the file in write binary mode 380 | FILE *fp = fopen(filename, "wb"); 381 | if (!fp) 382 | { 383 | // If the file could not be created/opened 384 | printf("File could not be opened"); 385 | char *res = "HTTP/1.1 404 Not Found\r\n\r\n"; // HTTP response 386 | bytesSent = send(client_fd, res, strlen(res), 0); 387 | } 388 | else 389 | { 390 | printf("Opening file %s\n", filename); 391 | } 392 | 393 | // Write the contents 394 | if (fwrite(content, 1, contentLength, fp) != contentLength) 395 | { 396 | printf("Error writing the data"); 397 | } 398 | 399 | fclose(fp); 400 | 401 | // Return contents 402 | char response[1024]; 403 | sprintf(response, "HTTP/1.1 201 Created\r\nContent-Type: application/octet-stream\r\nContent-Length: %d\r\n\r\n%s", contentLength, content); 404 | printf("Sending response: %s\n", response); 405 | bytesSent = send(client_fd, response, strlen(response), 0); 406 | } 407 | else 408 | { 409 | char *res = "HTTP/1.1 404 Not Found\r\n\r\n"; // HTTP response 410 | bytesSent = send(client_fd, res, strlen(res), 0); 411 | } 412 | 413 | if (bytesSent < 0) 414 | { 415 | printf("Send failed\n"); 416 | exit(1); 417 | } 418 | else 419 | { 420 | return; 421 | } 422 | } 423 | 424 | -------------------------------------------------------------------------------- /codecrafters.yml: -------------------------------------------------------------------------------- 1 | # Set this to true if you want debug logs. 2 | # 3 | # These can be VERY verbose, so we suggest turning them off 4 | # unless you really need them. 5 | debug: false 6 | 7 | # Use this to change the C version used to run your code 8 | # on Codecrafters. 9 | # 10 | # Available versions: c-9.2 11 | language_pack: c-9.2 12 | -------------------------------------------------------------------------------- /your_server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # DON'T EDIT THIS! 4 | # 5 | # CodeCrafters uses this file to test your code. Don't make any changes here! 6 | # 7 | # DON'T EDIT THIS! 8 | set -e 9 | tmpFile=$(mktemp) 10 | gcc -lcurl app/*.c -o $tmpFile 11 | exec "$tmpFile" "$@" 12 | --------------------------------------------------------------------------------