master xplshn/aruu / cmd / net / httpd.c
  1/* See LICENSE file for copyright and license details. */
  2
  3#include "arg.h"
  4#include "util.h"
  5
  6#include <arpa/inet.h>
  7#include <ctype.h>
  8#include <dirent.h>
  9#include <errno.h>
 10#include <fcntl.h>
 11#include <netdb.h>
 12#include <netinet/in.h>
 13#include <stdio.h>
 14#include <stdlib.h>
 15#include <string.h>
 16#include <sys/socket.h>
 17#include <sys/stat.h>
 18#include <sys/types.h>
 19#include <time.h>
 20#include <unistd.h>
 21
 22static void
 23usage(void)
 24{
 25  eprintf("usage: %s [-e string] [-d string] [-v] [dir]\n", argv0);
 26}
 27
 28static char *
 29url_decode(char *s)
 30{
 31  char        *r, *w;
 32  unsigned int val;
 33
 34  for (r = w = s; *r;) {
 35    if (*r == '%' && isxdigit((unsigned char)r[1]) && isxdigit((unsigned char)r[2])) {
 36      sscanf(r + 1, "%2x", &val);
 37      *w++ = val;
 38      r += 3;
 39    } else if (*r == '+') {
 40      *w++ = ' ';
 41      r++;
 42    } else {
 43      *w++ = *r++;
 44    }
 45  }
 46  *w = '\0';
 47  return s;
 48}
 49
 50static char *
 51url_encode(const char *s)
 52{
 53  char       *buf = emalloc(strlen(s) * 3 + 1);
 54  char       *w   = buf;
 55  const char *r   = s;
 56
 57  while (*r) {
 58    if (isalnum((unsigned char)*r) || strchr("-._~", *r)) {
 59      *w++ = *r++;
 60    } else {
 61      w += sprintf(w, "%%%02X", (unsigned char)*r);
 62      r++;
 63    }
 64  }
 65  *w = '\0';
 66  return buf;
 67}
 68
 69static void
 70send_error(int status, const char *msg)
 71{
 72  printf(
 73      "HTTP/1.1 %d %s\r\n"
 74      "Content-Type: text/html; charset=UTF-8\r\n"
 75      "Connection: close\r\n\r\n",
 76      status,
 77      msg
 78  );
 79  printf(
 80      "<html><head><title>%d %s</title></head>"
 81      "<body><h3>%d %s</h3></body></html>\n",
 82      status,
 83      msg,
 84      status,
 85      msg
 86  );
 87}
 88
 89static int
 90is_under(const char *file, const char *dir)
 91{
 92  char  *rfile = realpath(file, NULL);
 93  char  *rdir  = realpath(dir, NULL);
 94  int    rc    = 0;
 95  size_t len;
 96
 97  if (rfile && rdir) {
 98    len = strlen(rdir);
 99    if (strncmp(rfile, rdir, len) == 0) {
100      if (rfile[len] == '\0' || rfile[len] == '/' || (len > 0 && rdir[len - 1] == '/'))
101        rc = 1;
102    }
103  }
104  free(rfile);
105  free(rdir);
106  return rc;
107}
108
109static const char *
110get_mime_type(const char *file)
111{
112  const char *ext = strrchr(file, '.');
113  if (!ext)
114    return "application/octet-stream";
115  ext++;
116
117  if (strcasecmp(ext, "html") == 0 || strcasecmp(ext, "htm") == 0)
118    return "text/html; charset=UTF-8";
119  if (strcasecmp(ext, "css") == 0)
120    return "text/css";
121  if (strcasecmp(ext, "js") == 0)
122    return "application/javascript";
123  if (strcasecmp(ext, "png") == 0)
124    return "image/png";
125  if (strcasecmp(ext, "jpg") == 0 || strcasecmp(ext, "jpeg") == 0)
126    return "image/jpeg";
127  if (strcasecmp(ext, "gif") == 0)
128    return "image/gif";
129  if (strcasecmp(ext, "txt") == 0)
130    return "text/plain; charset=UTF-8";
131  if (strcasecmp(ext, "pdf") == 0)
132    return "application/pdf";
133  if (strcasecmp(ext, "zip") == 0)
134    return "application/zip";
135
136  return "application/octet-stream";
137}
138
139static void
140handle_connection(void)
141{
142  struct stat    st, st_idx;
143  struct dirent *de;
144  DIR           *dir;
145  char           line[4096];
146  char           method[32], path[2048], proto[32];
147  char           index_path[sizeof(path) + 32];
148  char           file_buf[8192];
149  char           date_buf[64];
150  const char    *mime_type;
151  char          *query, *p, *enc;
152  ssize_t        n_read;
153  size_t         len;
154  int            fd;
155
156  if (!fgets(line, sizeof(line), stdin))
157    return;
158
159  if (sscanf(line, "%31s %2047s %31s", method, path, proto) != 3) {
160    send_error(400, "Bad Request");
161    return;
162  }
163
164  while (fgets(line, sizeof(line), stdin)) {
165    if (line[0] == '\r' || line[0] == '\n')
166      break;
167  }
168
169  if (strcasecmp(method, "GET") != 0) {
170    send_error(501, "Not Implemented");
171    return;
172  }
173
174  url_decode(path);
175
176  query = strchr(path, '?');
177  if (query) {
178    *query = '\0';
179    setenv("QUERY_STRING", query + 1, 1);
180  } else {
181    unsetenv("QUERY_STRING");
182  }
183
184  p = path;
185  while (*p == '/')
186    p++;
187  if (*p == '\0')
188    p = ".";
189
190  if (stat(p, &st) < 0) {
191    send_error(404, "Not Found");
192    return;
193  }
194
195  if (!is_under(p, ".")) {
196    send_error(403, "Forbidden");
197    return;
198  }
199
200  if (S_ISDIR(st.st_mode)) {
201    len = strlen(path);
202    if (len > 0 && path[len - 1] != '/') {
203      printf(
204          "HTTP/1.1 302 Found\r\n"
205          "Location: %s/\r\n"
206          "Connection: close\r\n\r\n",
207          path
208      );
209      return;
210    }
211
212    snprintf(index_path, sizeof(index_path), "%s/index.html", p);
213    if (stat(index_path, &st_idx) == 0 && S_ISREG(st_idx.st_mode)) {
214      p  = index_path;
215      st = st_idx;
216      goto serve_file;
217    }
218
219    dir = opendir(p);
220    if (!dir) {
221      send_error(403, "Forbidden");
222      return;
223    }
224
225    printf(
226        "HTTP/1.1 200 OK\r\n"
227        "Content-Type: text/html; charset=UTF-8\r\n"
228        "Connection: close\r\n\r\n"
229    );
230    printf("<html><head><title>Index of %s</title></head><body>\n", path);
231    printf("<h3>Index of %s</h3><hr><pre>\n", path);
232
233    while ((de = readdir(dir))) {
234      if (strcmp(de->d_name, ".") == 0)
235        continue;
236      enc = url_encode(de->d_name);
237      printf("<a href=\"%s\">%s</a>\n", enc, de->d_name);
238      free(enc);
239    }
240    printf("</pre><hr></body></html>\n");
241    closedir(dir);
242    return;
243  }
244
245serve_file:
246  fd = open(p, O_RDONLY);
247  if (fd < 0) {
248    send_error(403, "Forbidden");
249    return;
250  }
251
252  mime_type = get_mime_type(p);
253  strftime(date_buf, sizeof(date_buf), "%a, %d %b %Y %H:%M:%S GMT", gmtime(&st.st_mtime));
254
255  printf(
256      "HTTP/1.1 200 OK\r\n"
257      "Content-Type: %s\r\n"
258      "Content-Length: %lld\r\n"
259      "Last-Modified: %s\r\n"
260      "Connection: close\r\n\r\n",
261      mime_type,
262      (long long)st.st_size,
263      date_buf
264  );
265
266  while ((n_read = read(fd, file_buf, sizeof(file_buf))) > 0) {
267    writeall(1, file_buf, n_read);
268  }
269  close(fd);
270}
271
272// ?man httpd: simple http daemon
273// ?man arguments: dir
274// ?man serve static files over http
275int
276main(int argc, char *argv[])
277{
278  char *eflag = NULL;
279  char *dflag = NULL;
280  char *enc;
281
282  ARGBEGIN
283  {
284    // ?man -e:str: specify expression or pattern
285    case 'e':
286      eflag = EARGF(usage());
287      break;
288    // ?man -d:str: specify directory
289    case 'd':
290      dflag = EARGF(usage());
291      break;
292    // ?man -v: verbose mode; show progress
293    case 'v':
294      break;
295    default:
296      usage();
297  }
298  ARGEND
299
300  if (eflag) {
301    enc = url_encode(eflag);
302    printf("%s\n", enc);
303    free(enc);
304    return 0;
305  }
306
307  if (dflag) {
308    printf("%s\n", url_decode(dflag));
309    return 0;
310  }
311
312  if (argc > 0) {
313    if (chdir(argv[0]) < 0)
314      eprintf("chdir %s:\n", argv[0]);
315  }
316
317  handle_connection();
318  return 0;
319}