xref: /aosp_15_r20/external/toybox/toys/net/httpd.c (revision cf5a6c84e2b8763fc1a7db14496fd4742913b199)
1 /* httpd.c - Web server.
2  *
3  * Copyright 2022 Rob Landley <[email protected]>
4  *
5  * See https://www.ietf.org/rfc/rfc2616.txt
6  *
7  * TODO: multiple domains, https, actual inetd with ratelimit...
8  * range, gzip, ETag (If-None-Match:, Last-Modified:), Date:
9  * "Accept-Ranges: bytes"/"Range: bytes=xxx-[yyy]"
10  * .htaccess (auth, forward)
11  * optional conf file, error pages
12  * -ifv -p [IP:]PORT -u [USER][:GRP] -c CFGFILE
13  * cgi: SERVER_PORT SERVER_NAME REMOTE_ADDR REMOTE_HOST REQUEST_METHOD
14 
15 USE_HTTPD(NEWTOY(httpd, ">1v", TOYFLAG_USR|TOYFLAG_BIN|TOYFLAG_LINEBUF))
16 
17 config HTTPD
18   bool "httpd"
19   default y
20   help
21     usage: httpd [-de STR] [-v] [DIR]
22 
23     Serve contents of directory as static web pages.
24 
25     -e	Escape STR as URL, printing result and exiting.
26     -d	Decode escaped STR, printing result and exiting.
27     -v	Verbose
28 */
29 
30 #define FOR_httpd
31 #include "toys.h"
32 
rfc1123(char * buf,time_t t)33 char *rfc1123(char *buf, time_t t)
34 {
35   strftime(buf, 64, "%a, %d %b %Y %T GMT", gmtime(&t));
36 
37   return buf;
38 }
39 
40 // She never told me...
mime(char * file)41 char *mime(char *file)
42 {
43   char *s = strrchr(file, '.'), *types[] = {
44     "asc\0text/plain", "bin\0application/octet-stream", "bmp\0image/bmp",
45     "cpio\0application/x-cpio", "css\0text/css", "doc\0application/msword",
46     "dtd\0text/xml", "dvi\0application/x-dvi", "gif\0image/gif",
47     "htm\0text/html", "html\0text/html", "jar\0applicat/x-java-archive",
48     "jpeg\0image/jpeg", "jpg\0image/jpeg", "js\0application/x-javascript",
49     "mp3\0audio/mpeg", "mp4\0video/mp4", "mpg\0video/mpeg",
50     "ogg\0application/ogg", "pbm\0image/x-portable-bitmap",
51     "pdf\0application/pdf", "png\0image/png",
52     "ppt\0application/vnd.ms-powerpoint", "ps\0application/postscript",
53     "rtf\0text/rtf", "sgml\0text/sgml", "svg\0image/svg+xml",
54     "tar\0application/x-tar", "tex\0application/x-tex", "tiff\0image/tiff",
55     "txt\0text/plain", "wav\0audio/x-wav", "xls\0application/vnd.ms-excel",
56     "xml\0tet/xml", "zip\0application/zip"
57   };
58   int i;
59 
60   strcpy(toybuf, "text/plain");
61   if (s++) for (i = 0; i<ARRAY_LEN(types); i++) {
62     if (strcasecmp(s, types[i])) continue;
63     strcpy(toybuf, types[i]+strlen(types[i])+1);
64     break;
65   }
66   if (!strncmp(toybuf, "text/", 5)) strcat(toybuf, "; charset=UTF-8");
67 
68   return toybuf;
69 }
70 
71 // Stop: header time.
header_time(int stat,char * str,char * more)72 static void header_time(int stat, char *str, char *more)
73 {
74   char buf[64];
75 
76   if (!more) more = "";
77   if (FLAG(v)) dprintf(2, "REPLY: %d %s\n%s\n", stat, str, more);
78   xprintf("HTTP/1.1 %d %s\r\nServer: toybox httpd/%s\r\nDate: %s\r\n%s"
79     "Connection: close\r\n\r\n", stat, str, TOYBOX_VERSION,
80     rfc1123(buf, time(0)), more);
81 }
82 
error_time(int stat,char * str)83 static void error_time(int stat, char *str)
84 {
85   header_time(stat, str, 0);
86   xprintf("<html><head><title>%d %s</title></head>"
87     "<body><h3>%d %s</h3></body></html>", stat, str, stat, str);
88 }
89 
isunder(char * file,char * dir)90 static int isunder(char *file, char *dir)
91 {
92   char *s1 = xabspath(dir, ABS_FILE), *s2 = xabspath(file, 0), *ss = s2;
93   int rc = s1 && s2 && strstart(&ss, s1) && (!*ss || *ss=='/' || ss[-1]=='/');
94 
95   free(s2);
96   free(s1);
97 
98   return rc;
99 }
100 
101 // Handle a connection on fd
handle(int infd,int outfd)102 void handle(int infd, int outfd)
103 {
104   FILE *fp = fdopen(infd, "r");
105   char *s = xgetline(fp), *cut, *ss, *esc, *path, *word[3];
106   int i = sizeof(toybuf), fd;
107 
108   if (!s) return;
109 
110   if (!getsockname(0, (void *)&toybuf, &i)) {
111     if (FLAG(v))
112       dprintf(2, "Hello %s\n%s\n", ntop((void *)toybuf), s);
113   }
114 
115   // Split line into method/path/protocol
116   for (i = 0, ss = s;;) {
117     word[i++] = ss;
118     while (*ss && !strchr(" \r\n", *ss)) ss++;
119     while (*ss && strchr(" \r\n", *ss)) *(ss++) = 0;
120     if (i==3) break;
121     if (!*ss) return header_time(400, "Bad Request", 0);
122   }
123 
124   // Process additional http/1.1 lines
125   while ((ss = xgetline(fp))) {
126     i = *chomp(ss);
127     if (FLAG(v)) dprintf(2, "%s\n", ss);
128 // TODO: any of
129 //User-Agent: Wget/1.20.1 (linux-gnu) - do we want to log anything?
130 //Accept: */* - 406 Too Snobbish
131 //Accept-Encoding: identity - we can gzip?
132 //Host: landley.net  - we could handle multiple domains?
133 //Connection: Keep-Alive - probably don't care
134 
135     free(ss);
136     if (!i) break;
137   }
138 
139   if (!strcasecmp(word[0], "get")) {
140     struct stat st;
141 
142     if (*(ss = word[1])!='/') error_time(400, "Bad Request");
143     while (*ss=='/') ss++;
144     if (!*ss) ss = "./";
145     else if ((cut = unescape_url(ss, 1))) setenv("QUERY_STRING", cut, 1);
146 
147     // TODO domain.com:/path/to/blah domain2.com:/path/to/that
148     // TODO cgi PATH_INFO /path/to/filename.cgi/and/more/stuff?path&info
149     if (!isunder(ss, ".") || stat(ss, &st)) error_time(404, "Not Found");
150     else if (-1 == (fd = open(ss, O_RDONLY))) error_time(403, "Forbidden");
151     else if (!S_ISDIR(st.st_mode)) {
152       char buf[64];
153 file:
154       header_time(200, "Ok", ss = xmprintf("Content-Type: %s\r\n"
155         "Content-Length: %lld\r\nLast-Modified: %s\r\n",
156         mime(ss), (long long)st.st_size, rfc1123(buf, st.st_mtime)));
157       free(ss);
158       xsendfile(fd, outfd);
159     } else if (ss[strlen(ss)-1]!='/') {
160       header_time(302, "Found", path = xmprintf("Location: %s/\r\n", word[1]));
161       free(path);
162     } else {
163       DIR *dd;
164       struct dirent *dir;
165 
166       // Do we have an index.html?
167       path = ss;
168       ss = "index.html";
169       path = xmprintf("%s%s", path, ss);
170       if (stat(path, &st) || !S_ISREG(st.st_mode)) i = -1;
171       else if (-1 == (i = open(path, O_RDONLY))) error_time(403, "Forbidden");
172       free(path);
173       if (i != -1) {
174         close(fd);
175         fd = i;
176 
177         goto file;
178       }
179 
180       // List directory contents
181       header_time(200, "Ok", "Content-Type: text/html\r\n");
182       dprintf(outfd, "<html><head><title>Index of %s</title></head>\n"
183         "<body><h3>Index of %s</h3></body>\n", word[1], word[1]);
184       for (dd = fdopendir(fd); (dir = readdir(dd));) {
185         esc = escape_url(dir->d_name, "<>&\"");
186         dprintf(outfd, "<a href=\"%s\">%s</a><br />\n", esc, esc);
187         free(esc);
188       }
189       dprintf(outfd, "</body></html>\n");
190     }
191   } else error_time(501, "Not Implemented");
192   free(s);
193 }
194 
httpd_main(void)195 void httpd_main(void)
196 {
197   if (toys.optc && chdir(*toys.optargs))
198     return error_time(500, "Internal Error");
199   // inetd only at the moment
200   handle(0, 1);
201 }
202