Initial commit of a basically working but severely unpolished
[sgt/agedu] / httpd.c
1 /*
2 * httpd.c: implementation of httpd.h.
3 */
4
5 #define _GNU_SOURCE
6
7 #include <stdio.h>
8 #include <stdlib.h>
9 #include <string.h>
10 #include <errno.h>
11 #include <assert.h>
12 #include <unistd.h>
13 #include <pwd.h>
14 #include <ctype.h>
15 #include <sys/types.h>
16 #include <sys/wait.h>
17 #include <fcntl.h>
18 #include <sys/socket.h>
19 #include <arpa/inet.h>
20 #include <netinet/in.h>
21 #include <syslog.h>
22
23 #include "malloc.h"
24 #include "html.h"
25
26 /* --- Logic driving what the web server's responses are. --- */
27
28 struct connctx {
29 const void *t;
30 char *data;
31 int datalen, datasize;
32 };
33
34 /*
35 * Called when a new connection arrives on a listening socket.
36 * Returns a connctx for the new connection.
37 */
38 struct connctx *new_connection(const void *t)
39 {
40 struct connctx *cctx = snew(struct connctx);
41 cctx->t = t;
42 cctx->data = NULL;
43 cctx->datalen = cctx->datasize = 0;
44 return cctx;
45 }
46
47 void free_connection(struct connctx *cctx)
48 {
49 sfree(cctx->data);
50 sfree(cctx);
51 }
52
53 static char *http_error(char *code, char *errmsg, char *errtext, ...)
54 {
55 return dupfmt("HTTP/1.1 %s %s\r\n"
56 "Date: %D\r\n"
57 "Server: agedu\r\n"
58 "Connection: close\r\n"
59 "Content-Type: text/html; charset=US-ASCII\r\n"
60 "\r\n"
61 "<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML 2.0//EN\">\r\n"
62 "<HTML><HEAD>\r\n"
63 "<TITLE>%s %s</TITLE>\r\n"
64 "</HEAD><BODY>\r\n"
65 "<H1>%s %s</H1>\r\n"
66 "<P>%s</P>\r\n"
67 "</BODY></HTML>\r\n", code, errmsg,
68 code, errmsg, code, errmsg, errtext);
69 }
70
71 static char *http_success(char *mimetype, int stuff_cr, char *document)
72 {
73 return dupfmt("HTTP/1.1 200 OK\r\n"
74 "Date: %D\r\n"
75 "Expires: %D\r\n"
76 "Server: agedu\r\n"
77 "Connection: close\r\n"
78 "Content-Type: %s\r\n"
79 "\r\n"
80 "%S", mimetype, stuff_cr, document);
81 }
82
83 /*
84 * Called when data comes in on a connection.
85 *
86 * If this function returns NULL, the platform code continues
87 * reading from the socket. Otherwise, it returns some dynamically
88 * allocated data which the platform code will then write to the
89 * socket before closing it.
90 */
91 char *got_data(struct connctx *ctx, char *data, int length, int magic_access)
92 {
93 char *line, *p, *q, *z1, *z2, c1, c2;
94 unsigned long index;
95 char *document, *ret;
96
97 if (ctx->datasize < ctx->datalen + length) {
98 ctx->datasize = (ctx->datalen + length) * 3 / 2 + 4096;
99 ctx->data = sresize(ctx->data, ctx->datasize, char);
100 }
101 memcpy(ctx->data + ctx->datalen, data, length);
102 ctx->datalen += length;
103
104 /*
105 * See if we have enough of an HTTP request to work out our
106 * response.
107 */
108 line = ctx->data;
109 /*
110 * RFC 2616 section 4.1: `In the interest of robustness, [...]
111 * if the server is reading the protocol stream at the
112 * beginning of a message and receives a CRLF first, it should
113 * ignore the CRLF.'
114 */
115 while (line - ctx->data < ctx->datalen &&
116 (*line == '\r' || *line == '\n'))
117 line++;
118
119 q = line;
120 while (q - ctx->data < ctx->datalen && *q != '\r' && *q != '\n')
121 q++;
122 if (q - ctx->data >= ctx->datalen)
123 return NULL; /* not got request line yet */
124
125 /*
126 * We've got the first line of the request, which is enough
127 * for us to work out what to say in response and start
128 * sending it. The platform side will keep reading data, but
129 * it'll ignore it.
130 *
131 * So, zero-terminate our line and parse it.
132 */
133 *q = '\0';
134 z1 = z2 = q;
135 c1 = c2 = *q;
136 p = line;
137 while (*p && !isspace((unsigned char)*p)) p++;
138 if (*p) {
139 z1 = p++;
140 c1 = *z1;
141 *z1 = '\0';
142 }
143 while (*p && isspace((unsigned char)*p)) p++;
144 q = p;
145 while (*q && !isspace((unsigned char)*q)) q++;
146 z2 = q;
147 c2 = *z2;
148 *z2 = '\0';
149
150 /*
151 * Now `line' points at the method name; p points at the URL,
152 * if any.
153 */
154
155 /*
156 * There should _be_ a URL, on any request type at all.
157 */
158 if (!*p) {
159 char *ret, *text;
160 *z2 = c2;
161 *z1 = c1;
162 text = dupfmt("<code>agedu</code> received the HTTP request"
163 " \"<code>%s</code>\", which contains no URL.",
164 line);
165 ret = http_error("400", "Bad request", text);
166 sfree(text);
167 return ret;
168 }
169
170 if (!magic_access) {
171 ret = http_error("403", "Forbidden",
172 "This is a restricted-access set of pages.");
173 } else {
174 p += strspn(p, "/?");
175 index = strtoul(p, NULL, 10);
176 document = html_query(ctx->t, index, "%lu");
177 if (document) {
178 ret = http_success("text/html", 1, document);
179 sfree(document);
180 } else {
181 ret = http_error("404", "Not Found",
182 "Pathname index out of range.");
183 }
184 }
185 return ret;
186 }
187
188 /* --- Platform support for running a web server. --- */
189
190 enum { FD_CLIENT, FD_LISTENER, FD_CONNECTION };
191
192 struct fd {
193 int fd;
194 int type;
195 int deleted;
196 char *wdata;
197 int wdatalen, wdatapos;
198 int magic_access;
199 struct connctx *cctx;
200 };
201
202 struct fd *fds = NULL;
203 int nfds = 0, fdsize = 0;
204
205 struct fd *new_fdstruct(int fd, int type)
206 {
207 struct fd *ret;
208
209 if (nfds >= fdsize) {
210 fdsize = nfds * 3 / 2 + 32;
211 fds = sresize(fds, fdsize, struct fd);
212 }
213
214 ret = &fds[nfds++];
215
216 ret->fd = fd;
217 ret->type = type;
218 ret->wdata = NULL;
219 ret->wdatalen = ret->wdatapos = 0;
220 ret->cctx = NULL;
221 ret->deleted = 0;
222 ret->magic_access = 0;
223
224 return ret;
225 }
226
227 void check_magic_access(struct fd *fd)
228 {
229 struct sockaddr_in sock, peer;
230 socklen_t addrlen;
231 char linebuf[4096], matchbuf[80];
232 FILE *fp;
233
234 addrlen = sizeof(sock);
235 if (getsockname(fd->fd, (struct sockaddr *)&sock, &addrlen)) {
236 fprintf(stderr, "getsockname: %s\n", strerror(errno));
237 exit(1);
238 }
239 addrlen = sizeof(peer);
240 if (getpeername(fd->fd, (struct sockaddr *)&peer, &addrlen)) {
241 fprintf(stderr, "getpeername: %s\n", strerror(errno));
242 exit(1);
243 }
244
245 sprintf(matchbuf, "%08X:%04X %08X:%04X",
246 peer.sin_addr.s_addr, ntohs(peer.sin_port),
247 sock.sin_addr.s_addr, ntohs(sock.sin_port));
248 fp = fopen("/proc/net/tcp", "r");
249 if (fp) {
250 while (fgets(linebuf, sizeof(linebuf), fp)) {
251 if (strlen(linebuf) >= 75 &&
252 !strncmp(linebuf+6, matchbuf, strlen(matchbuf))) {
253 int uid = atoi(linebuf + 75);
254 if (uid == getuid())
255 fd->magic_access = 1;
256 }
257 }
258 }
259 }
260
261 void run_httpd(const void *t)
262 {
263 int fd;
264 unsigned long ipaddr;
265 struct fd *f;
266 struct sockaddr_in addr;
267 socklen_t addrlen;
268
269 /*
270 * Establish the listening socket and retrieve its port
271 * number.
272 */
273 fd = socket(PF_INET, SOCK_STREAM, 0);
274 if (fd < 0) {
275 fprintf(stderr, "socket(PF_INET): %s\n", strerror(errno));
276 exit(1);
277 }
278 addr.sin_family = AF_INET;
279 srand(0L);
280 ipaddr = 0x7f000000;
281 ipaddr += (1 + rand() % 255) << 16;
282 ipaddr += (1 + rand() % 255) << 8;
283 ipaddr += (1 + rand() % 255);
284 addr.sin_addr.s_addr = htonl(ipaddr);
285 addr.sin_port = htons(0);
286 addrlen = sizeof(addr);
287 if (bind(fd, (struct sockaddr *)&addr, addrlen) < 0) {
288 fprintf(stderr, "bind: %s\n", strerror(errno));
289 exit(1);
290 }
291 if (listen(fd, 5) < 0) {
292 fprintf(stderr, "listen: %s\n", strerror(errno));
293 exit(1);
294 }
295 addrlen = sizeof(addr);
296 if (getsockname(fd, (struct sockaddr *)&addr, &addrlen)) {
297 fprintf(stderr, "getsockname: %s\n", strerror(errno));
298 exit(1);
299 }
300 printf("Server is at http://%s:%d/\n",
301 inet_ntoa(addr.sin_addr), ntohs(addr.sin_port));
302
303 /*
304 * Now construct an fd structure to hold it.
305 */
306 f = new_fdstruct(fd, FD_LISTENER);
307
308 /*
309 * Read from standard input, and treat EOF as a notification
310 * to exit.
311 */
312 new_fdstruct(0, FD_CLIENT);
313
314 /*
315 * Now we're ready to run our main loop. Keep looping round on
316 * select.
317 */
318 while (1) {
319 fd_set rfds, wfds;
320 int i, j, maxfd, ret;
321
322 #define FD_SET_MAX(fd, set, max) \
323 do { FD_SET((fd),(set)); (max) = ((max)<=(fd)?(fd)+1:(max)); } while(0)
324
325 /*
326 * Loop round the fd list putting fds into our select
327 * sets. Also in this loop we remove any that were marked
328 * as deleted in the previous loop.
329 */
330 FD_ZERO(&rfds);
331 FD_ZERO(&wfds);
332 maxfd = 0;
333 for (i = j = 0; j < nfds; j++) {
334
335 if (fds[j].deleted) {
336 sfree(fds[j].wdata);
337 free_connection(fds[j].cctx);
338 continue;
339 }
340 fds[i] = fds[j];
341
342 switch (fds[i].type) {
343 case FD_CLIENT:
344 case FD_LISTENER:
345 FD_SET_MAX(fds[i].fd, &rfds, maxfd);
346 break;
347 case FD_CONNECTION:
348 /*
349 * Always read from a connection socket. Even
350 * after we've started writing, the peer might
351 * still be sending (e.g. because we shamefully
352 * jumped the gun before waiting for the end of
353 * the HTTP request) and so we should be prepared
354 * to read data and throw it away.
355 */
356 FD_SET_MAX(fds[i].fd, &rfds, maxfd);
357 /*
358 * Also attempt to write, if we have data to write.
359 */
360 if (fds[i].wdatapos < fds[i].wdatalen)
361 FD_SET_MAX(fds[i].fd, &wfds, maxfd);
362 break;
363 }
364
365 i++;
366 }
367 nfds = i;
368
369 ret = select(maxfd, &rfds, &wfds, NULL, NULL);
370 if (ret <= 0) {
371 if (ret < 0 && (errno != EINTR)) {
372 fprintf(stderr, "select: %s", strerror(errno));
373 exit(1);
374 }
375 continue;
376 }
377
378 for (i = 0; i < nfds; i++) {
379 switch (fds[i].type) {
380 case FD_CLIENT:
381 if (FD_ISSET(fds[i].fd, &rfds)) {
382 char buf[4096];
383 int ret = read(fds[i].fd, buf, sizeof(buf));
384 if (ret <= 0) {
385 if (ret < 0) {
386 fprintf(stderr, "standard input: read: %s\n",
387 strerror(errno));
388 exit(1);
389 }
390 return;
391 }
392 }
393 break;
394 case FD_LISTENER:
395 if (FD_ISSET(fds[i].fd, &rfds)) {
396 /*
397 * New connection has come in. Accept it.
398 */
399 struct fd *f;
400 struct sockaddr_in addr;
401 socklen_t addrlen = sizeof(addr);
402 int newfd = accept(fds[i].fd, (struct sockaddr *)&addr,
403 &addrlen);
404 if (newfd < 0)
405 break; /* not sure what happened there */
406
407 f = new_fdstruct(newfd, FD_CONNECTION);
408 f->cctx = new_connection(t);
409 check_magic_access(f);
410 }
411 break;
412 case FD_CONNECTION:
413 if (FD_ISSET(fds[i].fd, &rfds)) {
414 /*
415 * There's data to be read.
416 */
417 char readbuf[4096];
418 int ret;
419
420 ret = read(fds[i].fd, readbuf, sizeof(readbuf));
421 if (ret <= 0) {
422 /*
423 * This shouldn't happen in a sensible
424 * HTTP connection, so we abandon the
425 * connection if it does.
426 */
427 close(fds[i].fd);
428 fds[i].deleted = 1;
429 break;
430 } else {
431 if (!fds[i].wdata) {
432 /*
433 * If we haven't got an HTTP response
434 * yet, keep processing data in the
435 * hope of acquiring one.
436 */
437 fds[i].wdata = got_data(fds[i].cctx, readbuf,
438 ret, fds[i].magic_access);
439 if (fds[i].wdata) {
440 fds[i].wdatalen = strlen(fds[i].wdata);
441 fds[i].wdatapos = 0;
442 }
443 } else {
444 /*
445 * Otherwise, just drop our read data
446 * on the floor.
447 */
448 }
449 }
450 }
451 if (FD_ISSET(fds[i].fd, &wfds) &&
452 fds[i].wdatapos < fds[i].wdatalen) {
453 /*
454 * The socket is writable, and we have data to
455 * write. Write it.
456 */
457 int ret = write(fds[i].fd, fds[i].wdata + fds[i].wdatapos,
458 fds[i].wdatalen - fds[i].wdatapos);
459 if (ret <= 0) {
460 /*
461 * Shouldn't happen; abandon the connection.
462 */
463 close(fds[i].fd);
464 fds[i].deleted = 1;
465 break;
466 } else {
467 fds[i].wdatapos += ret;
468 if (fds[i].wdatapos == fds[i].wdatalen) {
469 shutdown(fds[i].fd, SHUT_WR);
470 }
471 }
472 }
473 break;
474 }
475 }
476
477 }
478 }