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}