1 /*
2 This server creates a socket listening to PORT and
3 starts the event loop
4 2 mimetypes are set depending on file extension:
5 text/gemini .gmi
6 text/markdown .md .markdown
7 */
8
9 #define _GNU_SOURCE
10 #include <string.h>
11 #include <stdio.h>
12 #include <sys/types.h>
13 #include <sys/socket.h>
14 #include <netinet/in.h>
15 #include <stdlib.h>
16 #include <unistd.h>
17 #include <sys/types.h>
18 #include <sys/stat.h>
19 #include <stdbool.h>
20 #include <ctype.h>
21 #include <stdarg.h>
22 #include <limits.h>
23 #include <time.h>
24 #include <fcntl.h> // access
25
26 // inet_ntoa
27 //already included #include <sys/socket.h>
28 //already included #include <netinet/in.h>
29 #include <arpa/inet.h>
30
31
32 // server configuration
33 #define HOSTNAME "localhost"
34 #define ROOT "."
35 #define PORT 3000
36
37 bool isDir(const char *path) {
38 struct stat st;
39
40 if (stat(path, &st) == -1) {
41 return(false);
42 }
43
44 if (!S_ISDIR(st.st_mode)) {
45 return(false);
46 }
47 return(true);
48 }
49
50 int main(int ac, char **av){
51 int sock;
52 struct sockaddr_in server;
53 int mysock;
54 char buf[4096];
55 int rval;
56
57 char root[PATH_MAX] = {0};
58 if (ROOT[0] == '/') {
59 realpath(ROOT, root);
60 }
61 else {
62 // create absolute path from relative ROOT path
63 char p[PATH_MAX] = {0};
64 getcwd(p, PATH_MAX);
65 strcat(p, "/");
66 strcat(p, ROOT);
67 realpath(p, root);
68 strcat(root, "/");
69 }
70
71 size_t rootLen = strlen(root);
72 size_t slash = 0;
73
74 // count slashes at the end of root
75 // to compare paths with memcmp correctly
76 // since realpath removes the slashes at the
77 // end of the path from client.
78 while(root[rootLen-1-slash] == '/') {
79 slash++;
80 }
81
82 sock = socket(AF_INET, SOCK_STREAM, 0);
83 if (sock < 0){
84 perror("Failed to create socket");
85 }
86
87 server.sin_family = AF_INET;
88 server.sin_addr.s_addr = INADDR_ANY;
89 server.sin_port = htons(PORT);
90
91 /* setsockopt: Handy debugging trick that lets
92 * us rerun the server immediately after we kill it;
93 * otherwise we have to wait about 20 secs.
94 * Eliminates "ERROR on binding: Address already in use" error.
95 */
96 int optval = 1;
97 setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const void *)&optval , sizeof(int));
98
99 if (bind(sock, (struct sockaddr *) &server, sizeof(server))){
100 perror("bind failed");
101 exit(1);
102 }
103
104 listen(sock, SOMAXCONN);
105
106 printf("Serving "HOSTNAME":%d %s\n", PORT, root);
107
108 // date for request print
109 char date[50];
110 do {
111 // struct for printing client ip in terminal with inet_ntoa
112 struct sockaddr_in addr;
113 socklen_t len = sizeof(addr);
114 mysock = accept(sock, &addr, &len);
115 if (mysock == -1)
116 perror("accept failed");
117 else {
118 // Set 10s timeouts for receive and send (SO_RCVTIMEO and SO_SNDTIMEO)
119 struct timeval timeout;
120 timeout.tv_sec = 10;
121 timeout.tv_usec = 0;
122 // get YMD HMS date
123 time_t clk = time(NULL);
124 struct tm *pClk = localtime(&clk);
125 strftime(date, sizeof(date), "%Y-%m-%d:%H:%M:%S", pClk);
126 if (setsockopt(mysock, SOL_SOCKET, SO_RCVTIMEO, (void *) &timeout, sizeof(timeout)) < 0) {
127 printf("%s %s ",date, inet_ntoa(addr.sin_addr));
128 perror("receive timeout failed");
129 close(mysock);
130 continue;
131 }
132 if (setsockopt(mysock, SOL_SOCKET, SO_SNDTIMEO, (void *) &timeout, sizeof(timeout)) < 0) {
133 printf("%s %s ",date, inet_ntoa(addr.sin_addr));
134 perror("send timeout failed");
135 close(mysock);
136 continue;
137 }
138
139 // new client
140 memset(buf, 0, sizeof(buf));
141 if ((rval = recv(mysock, buf, sizeof(buf), 0)) < 0) {
142 printf("%s %s ",date, inet_ntoa(addr.sin_addr));
143 perror("reading message");
144 close(mysock);
145 continue;
146 }
147 else if (rval == 0) {
148 printf("%s %s ",date, inet_ntoa(addr.sin_addr));
149 puts("Ending connection");
150 close(mysock);
151 continue;
152 }
153
154 printf("%s %s ",date, inet_ntoa(addr.sin_addr));
155
156 // validate request then scan hostname path and content length
157
158 // find request end
159 char *reqEnd = memmem(buf, sizeof(buf), "\r\n", 2);
160 if (!reqEnd || buf[0] == ' ') {
161 puts("4 Invalid request");
162 // add MSG_NOSIGNAL flag to ignore SIGPIPE when the client closes the socket early
163 send(mysock, "4 Invalid request\r\n", sizeof("4 Invalid request\r\n"), MSG_NOSIGNAL);
164 close(mysock);
165 continue;
166 }
167
168 // check ascii
169 char *cursor = buf;
170 bool isBad = false;
171 while (cursor < reqEnd) {
172 if (*cursor < 32 || *cursor == 127) {
173 isBad = true;
174 break;
175 }
176 cursor++;
177 }
178 if (isBad) {
179 puts("4 Non ASCII");
180 send(mysock, "4 Non ASCII\r\n", sizeof("4 Non ASCII\r\n"), MSG_NOSIGNAL);
181 close(mysock);
182 continue;
183 }
184
185 // print request in terminal
186 *reqEnd = 0;
187 printf("%s ", buf);
188 *reqEnd = '\r';
189
190 // parse hostname, path and content_length in request
191 char *hostname;
192 char *path;
193 char *content_length;
194
195 hostname = buf;
196
197 // hostname must match HOSTNAME
198 // comment out this test to accept nay hostname
199 int c = memcmp(hostname, HOSTNAME, strlen(HOSTNAME));
200
201 if (c != 0) {
202 puts("4 Hostname");
203 send(mysock, "4 Hostname\r\n", sizeof("4 Hostname\r\n"), MSG_NOSIGNAL);
204 close(mysock);
205 continue;
206 }
207
208 // get path
209 cursor = buf;
210 while (*cursor != ' ' && cursor < reqEnd) {
211 cursor++;
212 }
213
214 cursor++;
215 if (cursor >= reqEnd || *cursor == ' ') {
216 puts("4 Path");
217 send(mysock, "4 Path\r\n", sizeof("4 Path\r\n"), MSG_NOSIGNAL);
218 close(mysock);
219 continue;
220 }
221
222 path = cursor;
223
224 // get content_length
225 while (*cursor != ' ' && cursor < reqEnd) {
226 cursor++;
227 }
228
229 cursor++;
230 if (cursor >= reqEnd || *cursor == ' ') {
231 puts("4 Length");
232 send(mysock, "4 Length\r\n", sizeof("4 Length\r\n"), MSG_NOSIGNAL);
233 close(mysock);
234 continue;
235 }
236
237 content_length = cursor;
238 while(cursor < reqEnd) {
239 if (!isdigit(*cursor)) {
240 isBad = true;
241 break;
242 }
243 cursor++;
244 }
245
246 // the request must not have any content
247 // content_length = 0
248 if (isBad || reqEnd - content_length > 1 || *content_length != '0') {
249 puts("4 Length");
250 send(mysock, "4 Length\r\n", sizeof("4 Length\r\n"), MSG_NOSIGNAL);
251 close(mysock);
252 continue;
253 }
254
255 // replace SPC with 0 at the end of path
256 *(content_length-1) = 0;
257
258 // build server path
259 char localPath[PATH_MAX] = {0};
260 if (rootLen + strlen(path) >= PATH_MAX) {
261 puts("5 Path too long");
262 send(mysock, "5 Path too long\r\n", sizeof("5 Path too long\r\n"), MSG_NOSIGNAL);
263 close(mysock);
264 continue;
265 }
266 memcpy(localPath, root, rootLen);
267 cursor = localPath + rootLen;
268 memcpy(cursor, path, strlen(path));
269
270 // check path
271 if (access(localPath, R_OK) == -1) {
272 puts("4 Not found");
273 send(mysock, "4 Not found\r\n", sizeof("4 Not found\r\n"), MSG_NOSIGNAL);
274 close(mysock);
275 continue;
276 }
277 char realPath[PATH_MAX] = {0};
278 realpath(localPath, realPath);
279 if (memcmp(realPath, root, rootLen-slash) != 0) {
280 puts("4 Not found");
281 send(mysock, "4 Not found\r\n", sizeof("4 Not found\r\n"), MSG_NOSIGNAL);
282 close(mysock);
283 continue;
284 }
285
286 size_t pathLen = strlen(realPath);
287 cursor = realPath + pathLen;
288 if (isDir(realPath)) {
289 if (pathLen + strlen("/index.gmi") >= PATH_MAX) {
290 puts("5 Path too long");
291 send(mysock, "5 Path too long\r\n", sizeof("5 Path too long\r\n"), MSG_NOSIGNAL);
292 close(mysock);
293 continue;
294 }
295 memcpy(cursor, "/index.gmi", strlen("/index.gmi"));
296 cursor += strlen("/index.gmi");
297 }
298
299 FILE *f = fopen(realPath, "r");
300 if (!f) {
301 puts("4 Page not found");
302 send(mysock, "4 Page not found\r\n", sizeof("4 Page not found\r\n"), MSG_NOSIGNAL);
303 close(mysock);
304 continue;
305 }
306
307 // request in buf is not needed anymore, reuse buf for response
308
309 // check gemini extension
310 char *mimetype = "application/octet-stream";
311 if (strlen(realPath) > 4 && memcmp(cursor-2, "md", 2) == 0) {
312 mimetype = "text/markdown";
313 }
314 else if (strlen(realPath) > 5 && memcmp(cursor-3, "gmi", 3) == 0) {
315 mimetype = "text/gemini";
316 }
317 else if (strlen(realPath) > 10 && memcmp(cursor-2, "markdown", 8) == 0) {
318 mimetype = "text/markdown";
319 }
320
321 int len = sprintf(buf, "2 %s\r\n", mimetype);
322 if (send(mysock, buf, len, MSG_NOSIGNAL) != -1) {
323 // print response in terminal
324 // remove \r\n
325 buf[len-2] = 0;
326 puts(buf);
327
328 // send file
329 size_t fsz;
330 while (fsz = fread(buf, 1, (size_t)sizeof(buf) , f)) {
331 ssize_t r = send(mysock, buf, fsz, MSG_NOSIGNAL);
332 if (r == -1) {
333 perror("write failed");
334 puts("closed socket");
335 break;
336 }
337 }
338 }
339 else {
340 perror("write failed");
341 }
342 fclose(f);
343 close(mysock);
344 }
345 } while(1);
346
347 puts("Server stopped.");
348 }