Created
March 3, 2026 20:26
-
-
Save peterhellberg/061cf3ae47f4f6941488e89d6296687b to your computer and use it in GitHub Desktop.
Very basic web server in C
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #include <arpa/inet.h> | |
| #include <netinet/in.h> | |
| #include <stdio.h> | |
| #include <stdlib.h> | |
| #include <string.h> | |
| #include <strings.h> | |
| #include <sys/socket.h> | |
| #include <sys/stat.h> | |
| #include <unistd.h> | |
| #define DEFAULT_PORT 9090 | |
| #define BUFFER_SIZE 4096 | |
| struct mime_map { | |
| const char *ext; | |
| const char *type; | |
| }; | |
| static const struct mime_map mime_types[] = { | |
| {"css", "text/css"}, {"html", "text/html"}, | |
| {"jpg", "image/jpeg"}, {"js", "application/javascript"}, | |
| {"json", "application/json"}, {"png", "image/png"}, | |
| {"txt", "text/plain"}, {"wasm", "application/wasm"}, | |
| }; | |
| static const char *get_mime_type(const char *path) { | |
| const char *ext = strrchr(path, '.'); | |
| if (!ext || ext == path) | |
| return "application/octet-stream"; | |
| ext++; // skip '.' | |
| for (size_t i = 0; i < sizeof(mime_types) / sizeof(mime_types[0]); i++) { | |
| if (strcasecmp(ext, mime_types[i].ext) == 0) { | |
| return mime_types[i].type; | |
| } | |
| } | |
| return "application/octet-stream"; | |
| } | |
| static int send_all(int sock, const void *buf, size_t len) { | |
| size_t total = 0; | |
| const char *p = buf; | |
| while (total < len) { | |
| ssize_t sent = send(sock, p + total, len - total, 0); | |
| if (sent <= 0) { | |
| return -1; | |
| } | |
| total += sent; | |
| } | |
| return 0; | |
| } | |
| static void send_response(int sock, int status, const char *status_text, | |
| const char *content_type, const void *body, | |
| size_t body_len) { | |
| char header[512]; | |
| int header_len = snprintf(header, sizeof(header), | |
| "HTTP/1.1 %d %s\r\n" | |
| "Content-Length: %zu\r\n" | |
| "Content-Type: %s\r\n" | |
| "Connection: close\r\n" | |
| "\r\n", | |
| status, status_text, body_len, content_type); | |
| send_all(sock, header, header_len); | |
| if (body && body_len > 0) { | |
| send_all(sock, body, body_len); | |
| } | |
| } | |
| static void send_404(int sock) { | |
| const char *body = | |
| "<html>\n" | |
| "<head>\n" | |
| " <title>404 Not Found</title>\n" | |
| " <style>\n" | |
| " body { background-color: #333; color: #fff; " | |
| "font-family: sans-serif; text-align: center; margin-top: 20%; }\n" | |
| " </style>\n" | |
| "</head>\n" | |
| "<body>\n" | |
| " <h1>404 Not Found</h1>\n" | |
| "</body>\n" | |
| "</html>\n"; | |
| send_response(sock, 404, "Not Found", "text/html", body, strlen(body)); | |
| } | |
| static void serve_file(int sock, const char *path) { | |
| struct stat st; | |
| FILE *f = | |
| (stat(path, &st) == 0 && S_ISREG(st.st_mode)) ? fopen(path, "rb") : NULL; | |
| if (!f) { | |
| send_404(sock); | |
| return; | |
| } | |
| const char *mime = get_mime_type(path); | |
| char header[512]; | |
| int header_len = snprintf(header, sizeof(header), | |
| "HTTP/1.1 200 OK\r\n" | |
| "Content-Length: %ld\r\n" | |
| "Content-Type: %s\r\n" | |
| "X-Content-Type-Options: nosniff\r\n" | |
| "Connection: close\r\n\r\n", | |
| (long)st.st_size, mime); | |
| if (header_len <= 0 || send_all(sock, header, header_len) < 0) { | |
| fclose(f); | |
| return; | |
| } | |
| char buf[BUFFER_SIZE]; | |
| size_t n; | |
| while ((n = fread(buf, 1, sizeof(buf), f)) > 0) | |
| if (send_all(sock, buf, n) < 0) | |
| break; | |
| fclose(f); | |
| } | |
| static void handle_client(int sock, const char *root) { | |
| char req[BUFFER_SIZE]; | |
| ssize_t len = recv(sock, req, sizeof(req) - 1, 0); | |
| if (len <= 0) | |
| return; | |
| req[len] = '\0'; | |
| char method[16], path[512]; | |
| if (sscanf(req, "%15s %511s", method, path) != 2) { | |
| send_404(sock); | |
| return; | |
| } | |
| if (strcmp(method, "GET") != 0 || strstr(path, "..")) { | |
| send_404(sock); | |
| return; | |
| } | |
| size_t path_len = strlen(path); | |
| if (path[path_len - 1] == '/') { | |
| if (path_len + strlen("index.html") < sizeof(path)) { | |
| strcat(path, "index.html"); | |
| } else { | |
| send_404(sock); | |
| return; | |
| } | |
| } | |
| char fullpath[1024]; | |
| snprintf(fullpath, sizeof(fullpath), "%s%s", root, path); | |
| serve_file(sock, fullpath); | |
| } | |
| static int parse_port(int argc, char *argv[]) { | |
| if (argc <= 1) | |
| return DEFAULT_PORT; | |
| char *endptr; | |
| long p = strtol(argv[1], &endptr, 10); | |
| if (*endptr != '\0' || p <= 0 || p > 65535) { | |
| fprintf(stderr, "Invalid port number: %s\n", argv[1]); | |
| exit(1); | |
| } | |
| return (int)p; | |
| } | |
| static const char *parse_root(int argc, char *argv[]) { | |
| if (argc > 2) | |
| return argv[2]; | |
| return "."; | |
| } | |
| static int setup_server(int port) { | |
| int sock = socket(AF_INET, SOCK_STREAM, 0); | |
| if (sock < 0) { | |
| perror("socket"); | |
| exit(1); | |
| } | |
| int opt = 1; | |
| setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); | |
| struct sockaddr_in addr = {.sin_family = AF_INET, | |
| .sin_addr.s_addr = INADDR_ANY, | |
| .sin_port = htons(port)}; | |
| if (bind(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0) { | |
| perror("bind"); | |
| exit(1); | |
| } | |
| if (listen(sock, 16) < 0) { | |
| perror("listen"); | |
| exit(1); | |
| } | |
| return sock; | |
| } | |
| static int accept_client(int server, char *ip_buf, size_t ip_len, | |
| int *client_port) { | |
| struct sockaddr_in client_addr; | |
| socklen_t len = sizeof(client_addr); | |
| int client = accept(server, (struct sockaddr *)&client_addr, &len); | |
| if (client < 0) { | |
| perror("accept"); | |
| return -1; | |
| } | |
| inet_ntop(AF_INET, &client_addr.sin_addr, ip_buf, ip_len); | |
| *client_port = ntohs(client_addr.sin_port); | |
| return client; | |
| } | |
| int main(int argc, char *argv[]) { | |
| const char *root = parse_root(argc, argv); | |
| int port = parse_port(argc, argv); | |
| int server = setup_server(port); | |
| printf("Serving files from %s on port %d\n", root, port); | |
| while (1) { | |
| char ip[INET_ADDRSTRLEN]; | |
| int client_port; | |
| int client = accept_client(server, ip, sizeof(ip), &client_port); | |
| if (client < 0) | |
| continue; | |
| printf("Client %s:%d connected\n", ip, client_port); | |
| { | |
| handle_client(client, root); | |
| shutdown(client, SHUT_WR); | |
| close(client); | |
| } | |
| printf("Client %s:%d disconnected\n", ip, client_port); | |
| } | |
| close(server); | |
| return 0; | |
| } |
Author
peterhellberg
commented
Mar 3, 2026
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment