├── .gitignore ├── README.md ├── exercises ├── 1.c ├── 2.c ├── 3.c ├── 4.c ├── 5.c ├── 6.c ├── blog │ └── index.html └── index.html ├── solutions ├── 1.c ├── 2.c ├── 3.c ├── 4.c ├── 5.c ├── 6.c ├── blog │ └── index.html └── index.html └── verify.c /.gitignore: -------------------------------------------------------------------------------- 1 | verify 2 | exercises/app1 3 | exercises/app2 4 | exercises/app3 5 | exercises/app4 6 | exercises/app5 7 | exercises/app6 8 | solutions/app1 9 | solutions/app2 10 | solutions/app3 11 | solutions/app4 12 | solutions/app5 13 | solutions/app6 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## C Fundamentals Course 2 | This is a companion repository for the [C Fundamentals](https://frontendmasters.com/courses/c-fundamentals/) course on Frontend Masters. 3 | [![Frontend Masters](https://static.frontendmasters.com/assets/brand/logos/full.png)](https://frontendmasters.com/courses/c-fundamentals/) 4 | 5 | Welcome! 6 | 7 | There are [slides for this workshop](https://static.frontendmasters.com/assets/courses/2025-06-09-c-fundamentals/c-fundamentals-slides.pdf) which reference the exercises in this repo. 8 | 9 | # Setup Instructions 10 | 11 | For this workshop, you'll need to be running one of these operating systems: 12 | * macOS 13 | * Linux, BSD, or another UNIX variant 14 | * [Linux Subsystem for Windows](https://learn.microsoft.com/en-us/windows/wsl/install) (regular Windows will *not* work for this workshop!) 15 | 16 | Any of these operating systems should have everything you need already installed. 17 | 18 | To verify this, clone this repository, then run the following terminal command in the root directory where you checked out the repo: 19 | 20 | ``` 21 | gcc -o verify verify.c && ./verify 22 | ``` 23 | 24 | It should print "You're all set!" 25 | 26 | ## Troubleshooting 27 | 28 | If running that command didn't print "You're all set!", you'll need to install either 29 | [GCC](https://gcc.gnu.org/) or [Clang](https://clang.llvm.org/) - 30 | either will work fine, so choose whichever you think will be 31 | easier to install. 32 | 33 | These exercises all say to run `gcc`, but you can subsitute `clang` for `gcc` and 34 | it should always work in the case of these examples; `clang` and `gcc` accept 35 | almost identical CLI flags. 36 | 37 | Fun fact: macOS actually ships with `clang` but aliases it to `gcc`, so if you run 38 | `gcc --version` on macOS, it prints out `Apple clang version ___`. 39 | -------------------------------------------------------------------------------- /exercises/1.c: -------------------------------------------------------------------------------- 1 | // #include 2 | #include 3 | 4 | // 👉 First, build and run the program. 5 | // 6 | // To do this, make sure you're in the `exercises` directory, and then run: 7 | // 8 | // gcc -o app1 1.c && ./app1 9 | 10 | int main() { 11 | // 👉 Try changing this string to "Hello, World!\n" - and also 12 | // changing the length to 14. 13 | // 👉 Try making the length less than 14. What happens when you run the program? 14 | // 👉 Try making the length much longer than 14, like 200. What happens? 😱 15 | write(1, "Hello, World!", 13); 16 | 17 | // 👉 Try uncommenting the next 2 lines, as well as the #include at the top. 18 | // (You'll want to change the length of write() above back to 14 first!) 19 | // 👉 Try adding a second number, named num2. Set it to something other than 42. 20 | // int num = 42; 21 | // printf("The number is: %d\n", num); 22 | 23 | // 👉 Try having printf print *both* numbers. 24 | // Hint: you'll need to make 2 changes to printf's arguments to do this! 25 | 26 | // 👉 Try returning something other than 0. (To see it, you'll need to run `echo $?` 27 | // immediately after the program finishes running.) 28 | return 0; 29 | } 30 | -------------------------------------------------------------------------------- /exercises/2.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | // 👉 First, build and run the program. 5 | // 6 | // To do this, make sure you're in the `exercises` directory, and then run: 7 | // 8 | // gcc -o app2 2.c && ./app2 9 | 10 | int main() { 11 | char *header = "HTTP/1.1 200 OK"; 12 | 13 | // 👉 Try replacing this `15` with a call to `strlen` (and `#include ` above!) 14 | write(1, header, 15); 15 | 16 | // 👉 After you're using `strlen` above, try adding a `\0` (with the backslash) 17 | // inside the definition of `header`, e.g. "HTT\0P/1.1 200 OK" - this inserts 18 | // a zero byte in the string. Before you run the program, what do you think it will print? 19 | 20 | // 👉 Try changing this `%s` to `%zu` (ignore the compiler warning). 21 | printf("\n\nThat output was from write(). This is from printf: %s\n", header); 22 | 23 | // 👉 Try changing the `%zud` back to `%s` and then replace the printf call's 24 | // last argument (originally `header`) with this argument instead: `(char*)123456` 25 | // 👉 Then try it with the number 0 instead of 123456. 26 | 27 | return 0; 28 | } 29 | -------------------------------------------------------------------------------- /exercises/3.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | // 👉 First, build and run the program. 5 | // 6 | // To do this, make sure you're in the `exercises` directory, and then run: 7 | // 8 | // gcc -o app3 3.c && ./app3 9 | 10 | const char* DEFAULT_FILE = "index.html"; 11 | 12 | char *to_path(char *req) { 13 | char *start, *end; 14 | 15 | // Advance `start` to the first space 16 | for (start = req; start[0] != ' '; start++) { 17 | if (!start[0]) { 18 | return NULL; 19 | } 20 | } 21 | 22 | start++; // Skip over the space 23 | 24 | // Advance `end` to the second space 25 | for (end = start; end[0] != ' '; end++) { 26 | if (!end[0]) { 27 | return NULL; 28 | } 29 | } 30 | 31 | // Ensure there's a '/' right before where we're about to copy in "index.html" 32 | if (end[-1] == '/') { 33 | end--; // We end in a slash, e.g. "/blog/" - so just move `end` to that slash. 34 | } else { 35 | end[0] = '/'; // We don't end in a slash, so write one. 36 | } 37 | 38 | // Copy in "index.html", overwriting whatever was there in the request string. 39 | memcpy( 40 | // 👉 Try refactoring out this + 1 by modifying the `if/else` above. 41 | end + 1, 42 | DEFAULT_FILE, 43 | // 👉 Try removing the +1 here. Re-run to see what happens, but first try to guess! 44 | strlen(DEFAULT_FILE) + 1 45 | ); 46 | 47 | return start; 48 | } 49 | 50 | int main() { 51 | // 👉 These three don't currently trim off the leading '/' - modify to_path to fix them! 52 | char req1[] = "GET /blog HTTP/1.1\nHost: example.com"; 53 | printf("Should be \"blog/index.html\": \"%s\"\n", to_path(req1)); 54 | 55 | char req2[] = "GET /blog/ HTTP/1.1\nHost: example.com"; 56 | printf("Should be \"blog/index.html\": \"%s\"\n", to_path(req2)); 57 | 58 | char req3[] = "GET / HTTP/1.1\nHost: example.com"; 59 | printf("Should be \"index.html\": \"%s\"\n", to_path(req3)); 60 | 61 | // 👉 Before fixing this next one, try moving it up to the beginning of main(). 62 | // What happens? 63 | 64 | // 👉 Finally, fix it by handling the case where `req` is too short to 65 | // have "index.html" memcpy'd into it. 66 | // Hint: `strlen()` returns an integer whose type is not `int` but rather `size_t` 67 | char req4[] = "GET /blog "; 68 | printf("Should be \"(null)\": \"%s\"\n", to_path(req4)); 69 | 70 | return 0; 71 | } 72 | -------------------------------------------------------------------------------- /exercises/4.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | // 👉 First, build and run the program. 8 | // 9 | // To do this, make sure you're in the `exercises` directory, and then run: 10 | // 11 | // gcc -o app4 4.c && ./app4 12 | 13 | const char* DEFAULT_FILE = "index.html"; 14 | 15 | char *to_path(char *req) { 16 | char *start, *end; 17 | 18 | // Advance `start` to the first space 19 | for (start = req; start[0] != ' '; start++) { 20 | if (!start[0]) { 21 | return NULL; 22 | } 23 | } 24 | 25 | start++; // Skip over the space 26 | 27 | // Advance `end` to the second space 28 | for (end = start; end[0] != ' '; end++) { 29 | if (!end[0]) { 30 | return NULL; 31 | } 32 | } 33 | 34 | // Ensure there's a '/' right before where we're about to copy in "index.html" 35 | if (end[-1] != '/') { 36 | end[0] = '/'; 37 | end++; 38 | } 39 | 40 | // If there isn't enough room to copy in "index.html" then return NULL. 41 | // (This only happens if the request has no headers, which should only 42 | // come up in practice if the request is malformed or something.) 43 | if (end + strlen(DEFAULT_FILE) > req + strlen(req)) { 44 | return NULL; 45 | } 46 | 47 | // Copy in "index.html", overwriting whatever was there in the request string. 48 | memcpy(end, DEFAULT_FILE, strlen(DEFAULT_FILE) + 1); 49 | 50 | return start + 1; // Skip the leading '/' (e.g. in "/blog/index.html") 51 | } 52 | 53 | void print_file(const char *path) { 54 | int fd = open(path, O_RDONLY); 55 | struct stat metadata; 56 | fstat(fd, &metadata); 57 | 58 | // 👉 Change this to `char *` and malloc(). (malloc comes from ) 59 | // Hint 1: Don't forget to handle the case where malloc returns NULL! 60 | // Hint 2: Don't forget to `free(buf)` later, to prevent memory leaks. 61 | char buf[metadata.st_size + 1]; 62 | 63 | ssize_t bytes_read = read(fd, buf, metadata.st_size); 64 | buf[bytes_read] = '\0'; 65 | printf("\n%s contents:\n\n%s\n", path, buf); 66 | 67 | close(fd); 68 | 69 | // 👉 Go back and add error handling for all the cases above where errors could happen. 70 | // (You can just printf that an error happened.) Some relevant docs: 71 | // 72 | // https://www.man7.org/linux/man-pages/man2/open.2.html 73 | // https://www.man7.org/linux/man-pages/man2/stat.2.html 74 | // https://www.man7.org/linux/man-pages/man2/read.2.html 75 | // https://www.man7.org/linux/man-pages/man3/malloc.3.html 76 | } 77 | 78 | int main() { 79 | char req1[] = "GET / HTTP/1.1\nHost: example.com"; 80 | print_file(to_path(req1)); 81 | 82 | char req2[] = "GET /blog HTTP/1.1\nHost: example.com"; 83 | print_file(to_path(req2)); 84 | 85 | 86 | return 0; 87 | } 88 | -------------------------------------------------------------------------------- /exercises/5.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | // 👉 First, build and run the program. 17 | // 18 | // To do this, make sure you're in the `exercises` directory, and then run: 19 | // 20 | // gcc -o app5 5.c && ./app5 21 | 22 | const int PORT = 8080; 23 | const int MAX_REQUEST_BYTES = 32768; 24 | 25 | const char* DEFAULT_FILE = "index.html"; 26 | 27 | char *to_path(char *req) { 28 | char *start, *end; 29 | 30 | // Advance `start` to the first space 31 | for (start = req; start[0] != ' '; start++) { 32 | if (!start[0]) { 33 | return NULL; 34 | } 35 | } 36 | 37 | start++; // Skip over the space 38 | 39 | // Advance `end` to the second space 40 | for (end = start; end[0] != ' '; end++) { 41 | if (!end[0]) { 42 | return NULL; 43 | } 44 | } 45 | 46 | // Ensure there's a '/' right before where we're about to copy in "index.html" 47 | if (end[-1] != '/') { 48 | end[0] = '/'; 49 | end++; 50 | } 51 | 52 | // If there isn't enough room to copy in "index.html" then return NULL. 53 | // (This only happens if the request has no headers, which should only 54 | // come up in practice if the request is malformed or something.) 55 | if (end + strlen(DEFAULT_FILE) > req + strlen(req)) { 56 | return NULL; 57 | } 58 | 59 | // Copy in "index.html", overwriting whatever was there in the request string. 60 | memcpy(end, DEFAULT_FILE, strlen(DEFAULT_FILE) + 1); 61 | 62 | return start + 1; // Skip the leading '/' (e.g. in "/blog/index.html") 63 | } 64 | 65 | int handle_req(char *request, int socket_fd) { 66 | char *path = to_path(request); 67 | 68 | if (path == NULL) { 69 | // 👉 Change this to send an the actual response to the socket. 70 | printf("HTTP/1.1 400 Bad Request\n\n"); 71 | return -1; 72 | } 73 | 74 | int fd = open(path, O_RDONLY); 75 | 76 | if (fd == -1) { 77 | // 👉 Change this to send an the actual response to the socket. 78 | if (errno == ENOENT) { 79 | // This one is easy to try out in a browser: visit something like 80 | // http://localhost:8080/foo (which doesn't exist, so it will 404.) 81 | // 82 | // Is the output in your terminal different from what you expected? 83 | // If so, you can get a clue to what's happening if you run this in a 84 | // different terminal window, while watching the output of your C program: 85 | // 86 | // wget http://localhost:8080/foo 87 | printf("HTTP/1.1 404 Not Found\n\n"); 88 | } else { 89 | printf("HTTP/1.1 500 Internal Server Error\n\n"); 90 | } 91 | 92 | return -1; 93 | } 94 | 95 | struct stat stats; 96 | 97 | // Populate the `stats` struct with the file's metadata 98 | // If it fails (even though the file was open), respond with a 500 error. 99 | if (fstat(fd, &stats) == -1) { 100 | // 👉 Change this to send an the actual response to the socket. 101 | printf("HTTP/1.1 500 Internal Server Error\n\n"); 102 | } 103 | 104 | // Write the header to the socket ("HTTP/1.1 200 OK") 105 | { 106 | const char *OK = "HTTP/1.1 200 OK\n\n"; 107 | size_t bytes_written = 0; 108 | size_t bytes_to_write = strlen(OK); 109 | 110 | while (bytes_to_write) { 111 | bytes_written = write(socket_fd, OK + bytes_written, bytes_to_write); 112 | 113 | if (bytes_written == -1) { 114 | // 👉 Change this to send an the actual response to the socket. 115 | printf("HTTP/1.1 500 Internal Server Error\n\n"); 116 | return -1; 117 | } 118 | 119 | bytes_to_write -= bytes_written; 120 | } 121 | } 122 | 123 | { 124 | // Read from the file and write to the socket 125 | char buffer[4096]; // Buffer for reading file data 126 | ssize_t bytes_read; 127 | 128 | // Loop until we've read the entire file 129 | while ((bytes_read = read(fd, buffer, sizeof(buffer))) > 0) { 130 | ssize_t bytes_written = 0; 131 | ssize_t bytes_remaining = bytes_read; 132 | 133 | // Ensure all bytes are written to the socket 134 | while (bytes_remaining > 0) { 135 | ssize_t result = write(socket_fd, buffer + bytes_written, bytes_remaining); 136 | 137 | if (result == -1) { 138 | // 👉 Change this to send an the actual response to the socket. 139 | printf("HTTP/1.1 500 Internal Server Error\n\n"); 140 | return -1; 141 | } 142 | 143 | bytes_written += result; 144 | bytes_remaining -= result; 145 | } 146 | } 147 | 148 | if (bytes_read == -1) { 149 | // 👉 Change this to send an the actual response to the socket. 150 | printf("HTTP/1.1 500 Internal Server Error\n\n"); 151 | return -1; 152 | } 153 | } 154 | 155 | close(fd); 156 | 157 | return 0; 158 | } 159 | 160 | int main() { 161 | int socket_fd = socket(AF_INET, SOCK_STREAM, 0); 162 | 163 | if (socket_fd == -1) { 164 | printf("opening socket failed.\n"); 165 | return -1; 166 | } 167 | 168 | // Prevent "Address in use" errors when restarting the server 169 | int opt = 1; 170 | if (setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) == -1) { 171 | printf("setting socket options failed.\n"); 172 | return -1; 173 | } 174 | 175 | struct sockaddr_in address; // IPv4 address 176 | 177 | address.sin_family = AF_INET; 178 | address.sin_addr.s_addr = INADDR_ANY; 179 | address.sin_port = htons(PORT); 180 | 181 | if (bind(socket_fd, (struct sockaddr *)&address, sizeof(address)) == -1) { 182 | printf("bind() failed.\n"); 183 | return -1; 184 | } 185 | 186 | if (listen(socket_fd, 4) == -1) { 187 | printf("listen() failed.\n"); 188 | return -1; 189 | } 190 | 191 | printf("Listening on port %d\n", PORT); 192 | 193 | char req[MAX_REQUEST_BYTES + 1]; // + 1 for null terminator 194 | int addrlen = sizeof(address); 195 | 196 | // Loop forever to keep processing new connections 197 | while (1) { 198 | // Block until we get a connection on the socket 199 | int req_socket_fd = accept(socket_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen); 200 | 201 | if (req_socket_fd >= 0) { 202 | // Read all the bytes from the socket into the buffer 203 | ssize_t bytes_read = read(req_socket_fd, req, MAX_REQUEST_BYTES); 204 | 205 | if (bytes_read < MAX_REQUEST_BYTES) { 206 | req[bytes_read] = '\0'; // Null-terminate 207 | 208 | // Parse the URL and method out of the HTTP request 209 | handle_req(req, req_socket_fd); 210 | } else { 211 | // The request was larger than the maximum size we support! 212 | 213 | // 👉 Change this to send an the actual response to the socket. 214 | printf("HTTP/1.1 413 Content Too Large\n\n"); 215 | } 216 | 217 | close(req_socket_fd); 218 | } else { 219 | // Continue listening for other connections even if accept fails 220 | } 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /exercises/6.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #ifdef __linux__ 17 | #include 18 | #endif 19 | 20 | // 👉 First, build and run the program. 21 | // 22 | // To do this, make sure you're in the `exercises` directory, and then run: 23 | // 24 | // gcc -o app6 6.c && ./app6 25 | 26 | #define PORT 8080 27 | #define MAX_REQUEST_BYTES 32768 28 | 29 | int respond_error(int socket_fd, int fd, const char *message) { 30 | if (fd != -1) { 31 | close(fd); 32 | } 33 | 34 | char response[256]; // These error responses are small, e.g. "500 Internal Server Error" 35 | snprintf(response, sizeof(response), "HTTP/1.1 %s\r\n\r\n", message); 36 | 37 | write(socket_fd, response, strlen(response)); 38 | 39 | return -1; 40 | } 41 | 42 | int respond_500(int socket_fd, int fd) { 43 | return respond_error(socket_fd, fd, "500 Internal Server Error"); 44 | } 45 | 46 | const char* DEFAULT_FILE = "index.html"; 47 | 48 | char *to_path(char *req) { 49 | char *start = req; 50 | 51 | while (start[0] != ' ') { 52 | start++; 53 | 54 | if (start[0] == 0) { 55 | return NULL; 56 | } 57 | } 58 | 59 | start++; 60 | 61 | char *end = start; 62 | char *last_slash = NULL; 63 | char *last_dot = NULL; 64 | 65 | while(end[0] != ' ') { 66 | switch (end[0]) { 67 | case '/': 68 | last_slash = end; 69 | break; 70 | case '.': 71 | last_dot = end; 72 | break; 73 | case '\0': 74 | return NULL; 75 | } 76 | 77 | end++; 78 | } 79 | 80 | // OPTIONS requests (and requests via proxies) may not start with '/' 81 | // For now, we don't support these requests. (We could always add support, though!) 82 | if (last_slash == NULL) { 83 | return NULL; 84 | } 85 | 86 | // If there's no file extension, default to index.html as the filename. 87 | if (last_dot == NULL || last_slash > last_dot) { 88 | end[0] = '/'; 89 | 90 | if (end > last_slash + 1) { 91 | end++; 92 | } 93 | 94 | // If there isn't enough room to copy in "index.html" then return NULL. 95 | // (This only happens if the request has no headers, which should only 96 | // come up in practice if the request is malformed or something.) 97 | if (end + strlen(DEFAULT_FILE) > req + strlen(req)) { 98 | return NULL; 99 | } 100 | 101 | memcpy(end, DEFAULT_FILE, strlen(DEFAULT_FILE) + 1); 102 | } else { 103 | end[0] = '\0'; 104 | } 105 | 106 | // Skip the leading '/' 107 | return start + 1; 108 | } 109 | 110 | // Macro to define a 4-char constant integer 111 | #define FOURCHAR(a, b, c, d) a | (b << 8) | (c << 16) | (d << 24) 112 | 113 | // Define constants for HTML and JPEG tags 114 | #define HTML FOURCHAR('h', 't', 'm', 'l') 115 | #define WASM FOURCHAR('w', 'a', 's', 'm') 116 | #define WEBP FOURCHAR('w', 'e', 'b', 'p') 117 | #define JPEG FOURCHAR('j', 'p', 'e', 'g') 118 | #define JPG FOURCHAR('j', 'p', 'g', 0) 119 | #define CSS FOURCHAR('c', 's', 's', 0) 120 | #define PNG FOURCHAR('p', 'n', 'g', 0) 121 | #define GIF FOURCHAR('g', 'i', 'f', 0) 122 | #define JS FOURCHAR('j', 's', 0, 0) 123 | 124 | size_t write_response_header(char ext[4], char *resp_str) { 125 | char *content_type; 126 | 127 | switch (*(int *)ext) { 128 | case HTML: 129 | content_type = "text/html"; 130 | break; 131 | case WASM: 132 | content_type = "application/wasm"; 133 | break; 134 | case CSS: 135 | content_type = "text/css"; 136 | break; 137 | case JS: 138 | content_type = "application/javascript"; 139 | break; 140 | case PNG: 141 | content_type = "image/png"; 142 | break; 143 | case JPG: 144 | content_type = "image/jpeg"; 145 | break; 146 | case JPEG: 147 | content_type = "image/jpeg"; 148 | break; 149 | case WEBP: 150 | content_type = "image/webp"; 151 | break; 152 | case GIF: 153 | content_type = "image/gif"; 154 | break; 155 | default: 156 | content_type = "application/octet-stream"; 157 | break; 158 | } 159 | 160 | return sprintf(resp_str, "HTTP/1.1 200 OK\r\nContent-Type: %s\r\n\r\n", content_type); 161 | } 162 | 163 | int handle_req(char *request, int socket_fd) { 164 | char *path = to_path(request); 165 | 166 | if (path == NULL) { 167 | return respond_error(socket_fd, -1, "400 Bad Request"); 168 | } 169 | 170 | int fd = open(path, O_RDONLY); 171 | 172 | if (fd == -1) { 173 | if (errno == ENOENT) { 174 | return respond_error(socket_fd, fd, "404 Not Found"); 175 | } else { 176 | return respond_500(socket_fd, fd); 177 | } 178 | } 179 | 180 | struct stat stats; 181 | 182 | // Populate the `stats` struct with the file's metadata 183 | // If it fails (even though the file was open), respond with a 500 error. 184 | if (fstat(fd, &stats) == -1) { 185 | return respond_500(socket_fd, fd); 186 | } 187 | 188 | // Write the header to the socket ("HTTP/1.1 200 OK" followed by a Content-Type header) 189 | { 190 | char ext[4] = {0}; 191 | 192 | { 193 | char *path_ext = strrchr(path, '.'); // Could skip the strrchr() if done in to_path 194 | size_t ext_len = strlen(path_ext); 195 | 196 | // If the extension is less than 4 bytes (5, counting the '.'), set it to NULL. 197 | if (path_ext != NULL && ext_len <= 5) { 198 | strncpy(ext, path_ext + 1, ext_len - 1); 199 | } 200 | } 201 | 202 | char resp_str[1024]; // Our responses are short, e.g. "200 OK\n\nContent-Type: text/css" 203 | size_t resp_length = write_response_header(ext, resp_str); 204 | ssize_t bytes_to_write = resp_length; 205 | ssize_t bytes_written = 0; 206 | 207 | while (bytes_to_write) { 208 | bytes_written = write(socket_fd, resp_str + bytes_written, bytes_to_write); 209 | 210 | if (bytes_written == -1) { 211 | // If sending the 200 didn't succeed, the odds of 500 succeeding aren't great! 212 | return respond_500(socket_fd, fd); 213 | } 214 | 215 | bytes_to_write -= bytes_written; 216 | } 217 | } 218 | 219 | { 220 | // Send the file's contents to the socket as the response body 221 | ssize_t bytes_to_send = stats.st_size; 222 | 223 | while (bytes_to_send > 0) { 224 | // 👉 `sendfile` works differently on different operating systems! 225 | // 226 | // Try implementing it for both operating systems (even though you can 227 | // only easily test it on the one you're currently running). 228 | #ifdef __linux__ 229 | // 👉 Replace these hardcoded integers by calling the Linux sendfile(). 230 | // Its docs are here: 231 | // https://www.man7.org/linux/man-pages/man2/sendfile.2.html#RETURN_VALUE 232 | ssize_t bytes_sent = 0; 233 | bool send_failed = 1; 234 | #elif defined(__APPLE__) 235 | // 👉 Replace these hardcoded integers by calling the macOS sendfile(). 236 | // Its docs are here: 237 | // https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/sendfile.2.html 238 | off_t bytes_sent = 0; 239 | bool send_failed = 1; 240 | #else 241 | #error "Unsupported operating system" 242 | #endif 243 | 244 | if (send_failed) { 245 | // We already sent a 200 OK response, so it's too late to send a 500. 246 | break; 247 | } 248 | 249 | bytes_to_send -= bytes_sent; 250 | } 251 | } 252 | 253 | close(fd); 254 | 255 | return 0; 256 | } 257 | 258 | int main() { 259 | int socket_fd = socket(AF_INET, SOCK_STREAM, 0); 260 | 261 | if (socket_fd == -1) { 262 | perror("Failed to open socket."); 263 | return -1; 264 | } 265 | 266 | // Prevent "Address in use" errors when restarting the server 267 | int opt = 1; 268 | if (setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) == -1) { 269 | perror("setsockopt"); 270 | return -1; 271 | } 272 | 273 | struct sockaddr_in address; // IPv4 address 274 | 275 | address.sin_family = AF_INET; 276 | address.sin_addr.s_addr = INADDR_ANY; 277 | address.sin_port = htons(PORT); 278 | 279 | if (bind(socket_fd, (struct sockaddr *)&address, sizeof(address)) == -1) { 280 | perror("bind failed"); 281 | return -1; 282 | } 283 | 284 | if (listen(socket_fd, 4) == -1) { 285 | perror("listen"); 286 | return -1; 287 | } 288 | 289 | printf("Listening on port %d\n", PORT); 290 | 291 | char req[MAX_REQUEST_BYTES + 1]; // + 1 for null terminator 292 | int addrlen = sizeof(address); 293 | 294 | // Loop forever to keep processing new connections 295 | while (1) { 296 | // Block until we get a connection on the socket 297 | int req_socket_fd = accept(socket_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen); 298 | 299 | if (req_socket_fd >= 0) { 300 | // Read all the bytes from the socket into the buffer 301 | ssize_t bytes_read = read(req_socket_fd, req, MAX_REQUEST_BYTES); 302 | 303 | if (bytes_read < MAX_REQUEST_BYTES) { 304 | req[bytes_read] = '\0'; // Null-terminate 305 | 306 | // Parse the URL and method out of the HTTP request 307 | handle_req(req, req_socket_fd); 308 | } else { 309 | // The request was larger than the maximum size we support! 310 | respond_error(socket_fd, -1, "413 Content Too Large"); 311 | } 312 | 313 | close(req_socket_fd); 314 | } else { 315 | perror("accept() failed."); 316 | // Continue listening for other connections even if accept fails 317 | } 318 | } 319 | } 320 | -------------------------------------------------------------------------------- /exercises/blog/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | This is a Blog 5 | 6 | 7 |

This is a Blog

8 |

Welcome to /blog

9 | 10 | 11 | -------------------------------------------------------------------------------- /exercises/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Hello, World! 5 | 6 | 7 |

This is the root / page

8 |

There's nothing here except a blog.

9 | 10 | 11 | -------------------------------------------------------------------------------- /solutions/1.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | int main() { 5 | write(1, "Hello, World!\n", 14); 6 | 7 | int num = 42; 8 | int num2 = 1337; 9 | printf("The number is: %d\nThe second number is: %d\n", num, num2); 10 | 11 | return 101; 12 | } 13 | -------------------------------------------------------------------------------- /solutions/2.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | int main() { 6 | char *header = "HTT\0P/1.1 200 OK"; 7 | 8 | write(1, header, strlen(header)); 9 | 10 | // 👉 Try changing this `%s` to `%zud` (ignore the compiler warning). 11 | printf("\n\nThat output was from write(). This is from printf: %s\n", (char*)0); 12 | 13 | return 0; 14 | } 15 | -------------------------------------------------------------------------------- /solutions/3.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | const char* DEFAULT_FILE = "index.html"; 5 | 6 | char *to_path(char *req, size_t req_len) { 7 | char *start, *end; 8 | 9 | // Advance `start` to the first space 10 | for (start = req; start[0] != ' '; start++) { 11 | if (!start[0]) { 12 | return NULL; 13 | } 14 | } 15 | 16 | start++; // Skip over the space 17 | 18 | // Advance `end` to the second space 19 | for (end = start; end[0] != ' '; end++) { 20 | if (!end[0]) { 21 | return NULL; 22 | } 23 | } 24 | 25 | // Ensure there's a '/' right before where we're about to copy in "index.html" 26 | if (end[-1] != '/') { 27 | end[0] = '/'; 28 | end++; 29 | } 30 | 31 | // If there isn't enough room to copy in "index.html" then return NULL. 32 | // (This only happens if the request has no headers, which should only 33 | // come up in practice if the request is malformed or something.) 34 | if (end + strlen(DEFAULT_FILE) > req + req_len) { 35 | return NULL; 36 | } 37 | 38 | // Copy in "index.html", overwriting whatever was there in the request string. 39 | memcpy(end, DEFAULT_FILE, strlen(DEFAULT_FILE) + 1); 40 | 41 | return start + 1; // Skip the leading '/' (e.g. in "/blog/index.html") 42 | } 43 | 44 | int main() { 45 | char req1[] = "GET /blog HTTP/1.1\nHost: example.com"; 46 | printf("Should be \"blog/index.html\": \"%s\"\n", to_path(req1, strlen(req1))); 47 | 48 | char req2[] = "GET /blog/ HTTP/1.1\nHost: example.com"; 49 | printf("Should be \"blog/index.html\": \"%s\"\n", to_path(req2, strlen(req2))); 50 | 51 | char req3[] = "GET / HTTP/1.1\nHost: example.com"; 52 | printf("Should be \"index.html\": \"%s\"\n", to_path(req3, strlen(req3))); 53 | 54 | char req4[] = "GET /blog "; 55 | printf("Should be \"(null)\": \"%s\"\n", to_path(req4, strlen(req4))); 56 | 57 | return 0; 58 | } 59 | -------------------------------------------------------------------------------- /solutions/4.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | const char* DEFAULT_FILE = "index.html"; 9 | 10 | char *to_path(char *req) { 11 | char *start, *end; 12 | 13 | // Advance `start` to the first space 14 | for (start = req; start[0] != ' '; start++) { 15 | if (!start[0]) { 16 | return NULL; 17 | } 18 | } 19 | 20 | start++; // Skip over the space 21 | 22 | // Advance `end` to the second space 23 | for (end = start; end[0] != ' '; end++) { 24 | if (!end[0]) { 25 | return NULL; 26 | } 27 | } 28 | 29 | // Ensure there's a '/' right before where we're about to copy in "index.html" 30 | if (end[-1] != '/') { 31 | end[0] = '/'; 32 | end++; 33 | } 34 | 35 | // If there isn't enough room to copy in "index.html" then return NULL. 36 | // (This only happens if the request has no headers, which should only 37 | // come up in practice if the request is malformed or something.) 38 | if (end + strlen(DEFAULT_FILE) > req + strlen(req)) { 39 | return NULL; 40 | } 41 | 42 | // Copy in "index.html", overwriting whatever was there in the request string. 43 | memcpy(end, DEFAULT_FILE, strlen(DEFAULT_FILE) + 1); 44 | 45 | return start + 1; // Skip the leading '/' (e.g. in "/blog/index.html") 46 | } 47 | 48 | void print_file(const char *path) { 49 | int fd = open(path, O_RDONLY); 50 | 51 | if (fd == -1) { 52 | printf("Error opening file %s\n", path); 53 | return; 54 | } 55 | 56 | struct stat metadata; 57 | 58 | if (fstat(fd, &metadata) == -1) { 59 | printf("Error getting file stats\n"); 60 | close(fd); 61 | return; 62 | } 63 | 64 | char *buf = malloc(metadata.st_size + 1); 65 | 66 | if (buf == NULL) { 67 | printf("Memory allocation failed\n"); 68 | close(fd); 69 | free(buf); 70 | return; 71 | } 72 | 73 | ssize_t bytes_read = read(fd, buf, metadata.st_size); 74 | if (bytes_read == -1) { 75 | printf("Error reading file\n"); 76 | close(fd); 77 | free(buf); 78 | return; 79 | } 80 | 81 | buf[bytes_read] = '\0'; 82 | printf("\n%s contents:\n\n%s\n", path, buf); 83 | 84 | close(fd); 85 | free(buf); 86 | } 87 | 88 | int main() { 89 | char req1[] = "GET / HTTP/1.1\nHost: example.com"; 90 | print_file(to_path(req1)); 91 | 92 | char req2[] = "GET /blog HTTP/1.1\nHost: example.com"; 93 | print_file(to_path(req2)); 94 | 95 | 96 | return 0; 97 | } 98 | -------------------------------------------------------------------------------- /solutions/5.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | const char* DEFAULT_FILE = "index.html"; 5 | 6 | char *to_path(char *req, size_t req_len) { 7 | char *start, *end; 8 | 9 | // Advance `start` to the first space 10 | for (start = req; start[0] != ' '; start++) { 11 | if (!start[0]) { 12 | return NULL; 13 | } 14 | } 15 | 16 | start++; // Skip over the space 17 | 18 | char *last_slash = NULL; 19 | char *last_dot = NULL; 20 | 21 | // Advance `end` to the second space 22 | for (end = start; end[0] != ' '; end++) { 23 | switch (end[0]) { 24 | case '/': 25 | last_slash = end; 26 | break; 27 | case '.': 28 | last_dot = end; 29 | break; 30 | case '\0': 31 | return NULL; 32 | } 33 | } 34 | 35 | // OPTIONS requests (and requests via proxies) may not start with '/' 36 | // For now, we don't support these requests. (We could always add support, though!) 37 | if (last_slash == NULL) { 38 | return NULL; 39 | } 40 | 41 | // If the path ends with a slash, default to index.html as the filename. 42 | if (last_dot == NULL || last_slash > last_dot) { 43 | last_slash++; 44 | 45 | // If there isn't enough room to copy in "index.html" then return NULL. 46 | // (This only happens if the request has no headers, which should only 47 | // come up in practice if the request is malformed or something.) 48 | if (last_slash + strlen(DEFAULT_FILE) > req + req_len) { 49 | return NULL; 50 | } 51 | 52 | // There will always be enough space here when receiving a request 53 | // from a normal browser, because after the target there will be " HTTP/1.1" 54 | // followed by a newline and at least one header. To be robust to requests 55 | // from other clients, we could accept the length of the request and only 56 | // do this if we have confirmed there's enough room. 57 | memcpy(last_slash, DEFAULT_FILE, strlen(DEFAULT_FILE) + 1); 58 | } else { 59 | end[0] = '\0'; 60 | } 61 | 62 | return start + 1; // Skip the leading '/' (e.g. in "/blog/index.html") 63 | } 64 | 65 | int main() { 66 | char req1[] = "GET /blog HTTP/1.1\nHost: example.com"; 67 | printf("Should be \"blog/index.html\": \"%s\"\n", to_path(req1, strlen(req1))); 68 | 69 | char req2[] = "GET /blog/ HTTP/1.1\nHost: example.com"; 70 | printf("Should be \"blog/index.html\": \"%s\"\n", to_path(req2, strlen(req2))); 71 | 72 | char req3[] = "GET / HTTP/1.1\nHost: example.com"; 73 | printf("Should be \"index.html\": \"%s\"\n", to_path(req3, strlen(req3))); 74 | 75 | char req4[] = "GET /blog "; 76 | printf("Should be \"(null)\": \"%s\"\n", to_path(req4, strlen(req4))); 77 | 78 | char req5[] = "GET /image.png HTTP/1.1\nHost: example.com"; 79 | printf("Should be \"image.png\": \"%s\"\n", to_path(req5, strlen(req5))); 80 | 81 | char req6[] = "GET /static/image.png HTTP/1.1\nHost: example.com"; 82 | printf("Should be \"static/image.png\": \"%s\"\n", to_path(req6, strlen(req6))); 83 | 84 | char req7[] = "GET /static/images/image.png HTTP/1.1\nHost: example.com"; 85 | printf("Should be \"static/images/image.png\": \"%s\"\n", to_path(req7, strlen(req7))); 86 | 87 | return 0; 88 | } 89 | -------------------------------------------------------------------------------- /solutions/6.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #ifdef __linux__ 17 | #include 18 | #endif 19 | 20 | #define PORT 8080 21 | #define MAX_REQUEST_BYTES 32768 22 | 23 | int respond_error(int socket_fd, int fd, const char *message) { 24 | if (fd != -1) { 25 | close(fd); 26 | } 27 | 28 | char response[256]; // These error responses are small, e.g. "500 Internal Server Error" 29 | snprintf(response, sizeof(response), "HTTP/1.1 %s\r\n\r\n", message); 30 | 31 | write(socket_fd, response, strlen(response)); 32 | 33 | return -1; 34 | } 35 | 36 | int respond_500(int socket_fd, int fd) { 37 | return respond_error(socket_fd, fd, "500 Internal Server Error"); 38 | } 39 | 40 | const char* DEFAULT_FILE = "index.html"; 41 | 42 | char *to_path(char *req) { 43 | char *start = req; 44 | 45 | while (start[0] != ' ') { 46 | start++; 47 | 48 | if (start[0] == 0) { 49 | return NULL; 50 | } 51 | } 52 | 53 | start++; 54 | 55 | char *end = start; 56 | char *last_slash = NULL; 57 | char *last_dot = NULL; 58 | 59 | while(end[0] != ' ') { 60 | switch (end[0]) { 61 | case '/': 62 | last_slash = end; 63 | break; 64 | case '.': 65 | last_dot = end; 66 | break; 67 | case '\0': 68 | return NULL; 69 | } 70 | 71 | end++; 72 | } 73 | 74 | // OPTIONS requests (and requests via proxies) may not start with '/' 75 | // For now, we don't support these requests. (We could always add support, though!) 76 | if (last_slash == NULL) { 77 | return NULL; 78 | } 79 | 80 | // If there's no file extension, default to index.html as the filename. 81 | if (last_dot == NULL || last_slash > last_dot) { 82 | end[0] = '/'; 83 | 84 | if (end > last_slash + 1) { 85 | end++; 86 | } 87 | 88 | // If there isn't enough room to copy in "index.html" then return NULL. 89 | // (This only happens if the request has no headers, which should only 90 | // come up in practice if the request is malformed or something.) 91 | if (end + strlen(DEFAULT_FILE) > req + strlen(req)) { 92 | return NULL; 93 | } 94 | 95 | memcpy(end, DEFAULT_FILE, strlen(DEFAULT_FILE) + 1); 96 | } else { 97 | end[0] = '\0'; 98 | } 99 | 100 | // Skip the leading '/' 101 | return start + 1; 102 | } 103 | 104 | // Macro to define a 4-char constant integer 105 | #define FOURCHAR(a, b, c, d) a | (b << 8) | (c << 16) | (d << 24) 106 | 107 | // Define constants for HTML and JPEG tags 108 | #define HTML FOURCHAR('h', 't', 'm', 'l') 109 | #define WASM FOURCHAR('w', 'a', 's', 'm') 110 | #define WEBP FOURCHAR('w', 'e', 'b', 'p') 111 | #define JPEG FOURCHAR('j', 'p', 'e', 'g') 112 | #define JPG FOURCHAR('j', 'p', 'g', 0) 113 | #define CSS FOURCHAR('c', 's', 's', 0) 114 | #define PNG FOURCHAR('p', 'n', 'g', 0) 115 | #define GIF FOURCHAR('g', 'i', 'f', 0) 116 | #define JS FOURCHAR('j', 's', 0, 0) 117 | 118 | size_t write_response_header(char ext[4], char *resp_str) { 119 | char *content_type; 120 | 121 | switch (*(int *)ext) { 122 | case HTML: 123 | content_type = "text/html"; 124 | break; 125 | case WASM: 126 | content_type = "application/wasm"; 127 | break; 128 | case CSS: 129 | content_type = "text/css"; 130 | break; 131 | case JS: 132 | content_type = "application/javascript"; 133 | break; 134 | case PNG: 135 | content_type = "image/png"; 136 | break; 137 | case JPG: 138 | content_type = "image/jpeg"; 139 | break; 140 | case JPEG: 141 | content_type = "image/jpeg"; 142 | break; 143 | case WEBP: 144 | content_type = "image/webp"; 145 | break; 146 | case GIF: 147 | content_type = "image/gif"; 148 | break; 149 | default: 150 | content_type = "application/octet-stream"; 151 | break; 152 | } 153 | 154 | return sprintf(resp_str, "HTTP/1.1 200 OK\r\nContent-Type: %s\r\n\r\n", content_type); 155 | } 156 | 157 | int handle_req(char *request, int socket_fd) { 158 | char *path = to_path(request); 159 | 160 | if (path == NULL) { 161 | return respond_error(socket_fd, -1, "400 Bad Request"); 162 | } 163 | 164 | int fd = open(path, O_RDONLY); 165 | 166 | if (fd == -1) { 167 | if (errno == ENOENT) { 168 | return respond_error(socket_fd, fd, "404 Not Found"); 169 | } else { 170 | return respond_500(socket_fd, fd); 171 | } 172 | } 173 | 174 | struct stat stats; 175 | 176 | // Populate the `stats` struct with the file's metadata 177 | // If it fails (even though the file was open), respond with a 500 error. 178 | if (fstat(fd, &stats) == -1) { 179 | return respond_500(socket_fd, fd); 180 | } 181 | 182 | // Write the header to the socket ("HTTP/1.1 200 OK" followed by a Content-Type header) 183 | { 184 | char ext[4] = {0}; 185 | 186 | { 187 | char *path_ext = strrchr(path, '.'); // Could skip the strrchr() if done in to_path 188 | size_t ext_len = strlen(path_ext); 189 | 190 | // If the extension is less than 4 bytes (5, counting the '.'), set it to NULL. 191 | if (path_ext != NULL && ext_len <= 5) { 192 | strncpy(ext, path_ext + 1, ext_len - 1); 193 | } 194 | } 195 | 196 | char resp_str[1024]; // Our responses are short, e.g. "200 OK\n\nContent-Type: text/css" 197 | size_t resp_length = write_response_header(ext, resp_str); 198 | ssize_t bytes_to_write = resp_length; 199 | ssize_t bytes_written = 0; 200 | 201 | while (bytes_to_write) { 202 | bytes_written = write(socket_fd, resp_str + bytes_written, bytes_to_write); 203 | 204 | if (bytes_written == -1) { 205 | // If sending the 200 didn't succeed, the odds of 500 succeeding aren't great! 206 | return respond_500(socket_fd, fd); 207 | } 208 | 209 | bytes_to_write -= bytes_written; 210 | } 211 | } 212 | 213 | { 214 | // Send the file's contents to the socket as the response body 215 | ssize_t bytes_to_send = stats.st_size; 216 | 217 | while (bytes_to_send > 0) { 218 | #ifdef __linux__ 219 | ssize_t bytes_sent = sendfile(socket_fd, fd, NULL, stats.st_size); 220 | bool send_failed = bytes_sent == -1; 221 | #elif defined(__APPLE__) 222 | off_t bytes_sent = stats.st_size; 223 | bool send_failed = sendfile(fd, socket_fd, 0, &bytes_sent, NULL, 0) == -1; 224 | #else 225 | #error "Unsupported operating system" 226 | #endif 227 | 228 | if (send_failed) { 229 | // We already sent a 200 OK response, so it's too late to send a 500. 230 | break; 231 | } 232 | 233 | bytes_to_send -= bytes_sent; 234 | } 235 | } 236 | 237 | close(fd); 238 | 239 | return 0; 240 | } 241 | 242 | int main() { 243 | int socket_fd = socket(AF_INET, SOCK_STREAM, 0); 244 | 245 | if (socket_fd == -1) { 246 | perror("Failed to open socket."); 247 | return -1; 248 | } 249 | 250 | // Prevent "Address in use" errors when restarting the server 251 | int opt = 1; 252 | if (setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) == -1) { 253 | perror("setsockopt"); 254 | return -1; 255 | } 256 | 257 | struct sockaddr_in address; // IPv4 address 258 | 259 | address.sin_family = AF_INET; 260 | address.sin_addr.s_addr = INADDR_ANY; 261 | address.sin_port = htons(PORT); 262 | 263 | if (bind(socket_fd, (struct sockaddr *)&address, sizeof(address)) == -1) { 264 | perror("bind failed"); 265 | return -1; 266 | } 267 | 268 | if (listen(socket_fd, 4) == -1) { 269 | perror("listen"); 270 | return -1; 271 | } 272 | 273 | printf("Listening on port %d\n", PORT); 274 | 275 | char req[MAX_REQUEST_BYTES + 1]; // + 1 for null terminator 276 | int addrlen = sizeof(address); 277 | 278 | // Loop forever to keep processing new connections 279 | while (1) { 280 | // Block until we get a connection on the socket 281 | int req_socket_fd = accept(socket_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen); 282 | 283 | if (req_socket_fd >= 0) { 284 | // Read all the bytes from the socket into the buffer 285 | ssize_t bytes_read = read(req_socket_fd, req, MAX_REQUEST_BYTES); 286 | 287 | if (bytes_read < MAX_REQUEST_BYTES) { 288 | req[bytes_read] = '\0'; // Null-terminate 289 | 290 | // Parse the URL and method out of the HTTP request 291 | handle_req(req, req_socket_fd); 292 | } else { 293 | // The request was larger than the maximum size we support! 294 | respond_error(socket_fd, -1, "413 Content Too Large"); 295 | } 296 | 297 | close(req_socket_fd); 298 | } else { 299 | perror("accept() failed."); 300 | // Continue listening for other connections even if accept fails 301 | } 302 | } 303 | } 304 | -------------------------------------------------------------------------------- /solutions/blog/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | This is a Blog 5 | 6 | 7 |

This is a Blog

8 |

Welcome to /blog

9 | 10 | 11 | -------------------------------------------------------------------------------- /solutions/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Hello, World! 5 | 6 | 7 |

This is the root / page

8 |

There's nothing here except a blog.

9 | 10 | 11 | -------------------------------------------------------------------------------- /verify.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int main() { 4 | write(1, "You're all set!\n", 16); 5 | return 0; 6 | } 7 | --------------------------------------------------------------------------------