httpd

a small HTTP server written in C using POSIX sockets


httpd / httpd.c

httpd.c (9.1K) raw | download
1#define _POSIX_C_SOURCE 200809L
2#include <sys/socket.h>
3#include <sys/stat.h>
4#include <sys/wait.h>
5#include <netinet/in.h>
6#include <signal.h>
7#include <fcntl.h>
8#include <unistd.h>
9#include <errno.h>
10#include <inttypes.h>
11#include <stdio.h>
12#include <stdlib.h>
13#include <string.h>
14
15static int
16write_all(int fd, const void *data, size_t len)
17{
18 const char *p = data;
19 while (len > 0) {
20 ssize_t n = write(fd, p, len);
21 if (n < 0 && errno == EINTR)
22 continue;
23 if (n <= 0)
24 return -1;
25 p += n;
26 len -= (size_t)n;
27 }
28 return 0;
29}
30
31/* Bound both individual lines and the total request headers. */
32static int
33read_line(int fd, char *line, size_t size, size_t *total)
34{
35 size_t len = 0;
36 for (;;) {
37 unsigned char c;
38 ssize_t n = read(fd, &c, 1);
39 if (n < 0 && errno == EINTR)
40 continue;
41 if (n != 1 || ++*total > 65536 || c == '\0')
42 return -1;
43 if (c == '\n') {
44 if (len == 0 || line[len - 1] != '\r')
45 return -1;
46 line[len - 1] = '\0';
47 return 0;
48 }
49 if (len + 1 >= size)
50 return -1;
51 line[len++] = (char)c;
52 }
53}
54
55static int
56send_headers(int fd, int status, const char *reason, const char *type,
57 off_t length)
58{
59 char buf[512];
60 int n = snprintf(buf, sizeof(buf),
61 "HTTP/1.0 %d %s\r\nContent-Length: %jd\r\n"
62 "Content-Type: %s\r\nConnection: close\r\n%s\r\n",
63 status, reason, (intmax_t)length, type,
64 status == 405 ? "Allow: GET\r\n" : "");
65 if (n < 0 || (size_t)n >= sizeof(buf))
66 return -1;
67 return write_all(fd, buf, (size_t)n);
68}
69
70static void
71send_error(int fd, int status)
72{
73 const char *reason;
74 char body[80];
75 int n;
76 switch (status) {
77 case 400: reason = "Bad Request"; break;
78 case 403: reason = "Forbidden"; break;
79 case 404: reason = "Not Found"; break;
80 case 405: reason = "Method Not Allowed"; break;
81 default: status = 500; reason = "Internal Server Error"; break;
82 }
83 n = snprintf(body, sizeof(body), "%d %s\n", status, reason);
84 if (send_headers(fd, status, reason, "text/plain", n) == 0)
85 (void)write_all(fd, body, (size_t)n);
86}
87
88static const char *
89mime_type(const char *path)
90{
91 const char *ext = strrchr(path, '.');
92 if (ext == NULL) return "application/octet-stream";
93 if (strcmp(ext, ".html") == 0) return "text/html";
94 if (strcmp(ext, ".css") == 0) return "text/css";
95 if (strcmp(ext, ".js") == 0) return "text/javascript";
96 if (strcmp(ext, ".txt") == 0) return "text/plain";
97 if (strcmp(ext, ".png") == 0) return "image/png";
98 if (strcmp(ext, ".jpg") == 0 || strcmp(ext, ".jpeg") == 0)
99 return "image/jpeg";
100 if (strcmp(ext, ".gif") == 0) return "image/gif";
101 if (strcmp(ext, ".svg") == 0) return "image/svg+xml";
102 return "application/octet-stream";
103}
104
105static int
106hex_digit(unsigned char c)
107{
108 if (c >= '0' && c <= '9') return c - '0';
109 if (c >= 'a' && c <= 'f') return c - 'a' + 10;
110 if (c >= 'A' && c <= 'F') return c - 'A' + 10;
111 return -1;
112}
113
114/* Decode before checking components, so encoded '..' cannot bypass checks. */
115static int
116decode_path(char *path)
117{
118 char *src = path, *dst = path;
119 if (*src != '/')
120 return -1;
121 while (*src && *src != '?') {
122 unsigned char c = (unsigned char)*src++;
123 if (c == '%') {
124 int hi, lo;
125 if (!src[0] || !src[1]) return -1;
126 hi = hex_digit((unsigned char)src[0]);
127 lo = hex_digit((unsigned char)src[1]);
128 if (hi < 0 || lo < 0) return -1;
129 c = (unsigned char)(hi * 16 + lo);
130 src += 2;
131 }
132 if (c < 32 || c == 127 || c == '\\' || c == '#')
133 return -1;
134 *dst++ = (char)c;
135 }
136 *dst = '\0';
137 if (strcmp(path, "/") == 0)
138 strcpy(path, "/index.html");
139 return 0;
140}
141
142static int
143file_error(int error)
144{
145 if (error == ENOENT || error == ENOTDIR) return 404;
146 if (error == EACCES || error == EPERM || error == ELOOP) return 403;
147 return 500;
148}
149
150/* Walk relative to the root descriptor; never follow symbolic links. */
151static int
152open_file(int root, char *path, int *status)
153{
154 char *part, *next;
155 int dir = dup(root), fd;
156 if (dir < 0) {
157 *status = 500;
158 return -1;
159 }
160 part = path + 1;
161 for (;;) {
162 next = strchr(part, '/');
163 if (next != NULL) *next = '\0';
164 if (*part == '\0' || strcmp(part, ".") == 0 ||
165 strcmp(part, "..") == 0) {
166 *status = 403;
167 close(dir);
168 return -1;
169 }
170 do {
171 fd = openat(dir, part, O_RDONLY | O_NOFOLLOW | O_NONBLOCK |
172 (next != NULL ? O_DIRECTORY : 0));
173 } while (fd < 0 && errno == EINTR);
174 if (fd < 0) *status = file_error(errno);
175 close(dir);
176 if (fd < 0 || next == NULL)
177 return fd;
178 dir = fd;
179 part = next + 1;
180 }
181}
182
183static void
184handle_client(int client, int root)
185{
186 char line[8192], method[32], path[8192], version[16], buf[16384];
187 const char *type;
188 size_t total = 0;
189 struct stat st;
190 int end = 0, fd, status = 500;
191 off_t remaining;
192
193 if (read_line(client, line, sizeof(line), &total) < 0 ||
194 sscanf(line, "%31s %8191s %15s%n", method, path, version, &end) != 3 ||
195 line[end] != '\0' ||
196 (strcmp(version, "HTTP/1.0") != 0 &&
197 strcmp(version, "HTTP/1.1") != 0)) {
198 send_error(client, 400);
199 return;
200 }
201 for (;;) {
202 if (read_line(client, line, sizeof(line), &total) < 0) {
203 send_error(client, 400);
204 return;
205 }
206 if (*line == '\0') break;
207 if (strchr(line, ':') == NULL || line[0] == ':') {
208 send_error(client, 400);
209 return;
210 }
211 }
212 if (strcmp(method, "GET") != 0) {
213 send_error(client, 405);
214 return;
215 }
216 if (decode_path(path) < 0) {
217 send_error(client, 400);
218 return;
219 }
220 type = mime_type(path);
221 fd = open_file(root, path, &status);
222 if (fd < 0) {
223 send_error(client, status);
224 return;
225 }
226 if (fstat(fd, &st) < 0) {
227 send_error(client, 500);
228 close(fd);
229 return;
230 }
231 if (!S_ISREG(st.st_mode)) {
232 send_error(client, 403);
233 close(fd);
234 return;
235 }
236 remaining = st.st_size;
237 if (send_headers(client, 200, "OK", type, remaining) == 0) {
238 while (remaining > 0) {
239 size_t count = remaining < (off_t)sizeof(buf) ?
240 (size_t)remaining : sizeof(buf);
241 ssize_t n = read(fd, buf, count);
242 if (n < 0 && errno == EINTR) continue;
243 if (n <= 0 || write_all(client, buf, (size_t)n) < 0) break;
244 remaining -= n;
245 }
246 }
247 close(fd);
248}
249
250static void
251reap_children(int sig)
252{
253 int saved = errno;
254 pid_t pid;
255 (void)sig;
256 do {
257 pid = waitpid(-1, NULL, WNOHANG);
258 } while (pid > 0 || (pid < 0 && errno == EINTR));
259 errno = saved;
260}
261
262int
263main(int argc, char **argv)
264{
265 const char *directory = argc > 2 ? argv[2] : ".";
266 long port = 8080;
267 char *end;
268 int root, listener, one = 1;
269 struct sockaddr_in address = {0};
270 struct sigaction action = {0};
271
272 if (argc > 3) {
273 fprintf(stderr, "usage: %s [port] [directory]\n", argv[0]);
274 return 1;
275 }
276 if (argc > 1) {
277 errno = 0;
278 port = strtol(argv[1], &end, 10);
279 if (errno || *argv[1] == '\0' || *end || port < 1 || port > 65535) {
280 fprintf(stderr, "invalid port: %s\n", argv[1]);
281 return 1;
282 }
283 }
284 root = open(directory, O_RDONLY | O_DIRECTORY);
285 if (root < 0) { perror(directory); return 1; }
286 action.sa_handler = SIG_IGN;
287 sigemptyset(&action.sa_mask);
288 if (sigaction(SIGPIPE, &action, NULL) < 0) goto fail;
289 action.sa_handler = reap_children;
290 action.sa_flags = SA_RESTART | SA_NOCLDSTOP;
291 if (sigaction(SIGCHLD, &action, NULL) < 0) goto fail;
292 listener = socket(AF_INET, SOCK_STREAM, 0);
293 if (listener < 0) goto fail;
294 address.sin_family = AF_INET;
295 address.sin_addr.s_addr = htonl(INADDR_ANY);
296 address.sin_port = htons((unsigned short)port);
297 if (setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)) < 0 ||
298 bind(listener, (struct sockaddr *)&address, sizeof(address)) < 0 ||
299 listen(listener, 64) < 0) {
300 int saved = errno;
301 close(listener);
302 errno = saved;
303 goto fail;
304 }
305 printf("listening on port %ld\n", port);
306 fflush(stdout);
307 for (;;) {
308 int client = accept(listener, NULL, NULL);
309 pid_t pid;
310 if (client < 0) {
311 if (errno == EINTR || errno == ECONNABORTED) continue;
312 perror("accept");
313 break;
314 }
315 pid = fork();
316 if (pid == 0) {
317 close(listener);
318 handle_client(client, root);
319 close(client);
320 close(root);
321 _exit(0);
322 }
323 if (pid < 0) perror("fork");
324 close(client);
325 }
326 close(listener);
327 close(root);
328 return 1;
329fail:
330 perror("httpd");
331 close(root);
332 return 1;
333}