💾 Archived View for gmi.noulin.net › gitRepositories › spartserv › file › spartserv.c.gmi captured on 2023-01-29 at 13:30:29. Gemini links have been rewritten to link to archived content
-=-=-=-=-=-=-
spartserv.c (10681B)
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 if (setsockopt(mysock, SOL_SOCKET, SO_RCVTIMEO, (void *) &timeout, sizeof(timeout)) < 0) { 123 perror("receive timeout failed"); 124 close(mysock); 125 continue; 126 } 127 if (setsockopt(mysock, SOL_SOCKET, SO_SNDTIMEO, (void *) &timeout, sizeof(timeout)) < 0) { 128 perror("send timeout failed"); 129 close(mysock); 130 continue; 131 } 132 133 // new client 134 memset(buf, 0, sizeof(buf)); 135 if ((rval = recv(mysock, buf, sizeof(buf), 0)) < 0) { 136 perror("reading message"); 137 close(mysock); 138 continue; 139 } 140 else if (rval == 0) { 141 puts("Ending connection"); 142 close(mysock); 143 continue; 144 } 145 146 // get YMD HMS date 147 time_t clk = time(NULL); 148 struct tm *pClk = localtime(&clk); 149 strftime(date, sizeof(date), "%Y-%m-%d:%H:%M:%S", pClk); 150 printf("%s %s ",date, inet_ntoa(addr.sin_addr)); 151 152 // validate request then scan hostname path and content length 153 154 // find request end 155 char *reqEnd = memmem(buf, sizeof(buf), "\r\n", 2); 156 if (!reqEnd || buf[0] == ' ') { 157 puts("4 Invalid request"); 158 // add MSG_NOSIGNAL flag to ignore SIGPIPE when the client closes the socket early 159 send(mysock, "4 Invalid request\r\n", sizeof("4 Invalid request\r\n"), MSG_NOSIGNAL); 160 close(mysock); 161 continue; 162 } 163 164 // check ascii 165 char *cursor = buf; 166 bool isBad = false; 167 while (cursor < reqEnd) { 168 if (*cursor < 32 || *cursor == 127) { 169 isBad = true; 170 break; 171 } 172 cursor++; 173 } 174 if (isBad) { 175 puts("4 Non ASCII"); 176 send(mysock, "4 Non ASCII\r\n", sizeof("4 Non ASCII\r\n"), MSG_NOSIGNAL); 177 close(mysock); 178 continue; 179 } 180 181 // print request in terminal 182 *reqEnd = 0; 183 printf("%s ", buf); 184 *reqEnd = '\r'; 185 186 // parse hostname, path and content_length in request 187 char *hostname; 188 char *path; 189 char *content_length; 190 191 hostname = buf; 192 193 // hostname must match HOSTNAME 194 // comment out this test to accept nay hostname 195 int c = memcmp(hostname, HOSTNAME, strlen(HOSTNAME)); 196 197 if (c != 0) { 198 puts("4 Hostname"); 199 send(mysock, "4 Hostname\r\n", sizeof("4 Hostname\r\n"), MSG_NOSIGNAL); 200 close(mysock); 201 continue; 202 } 203 204 // get path 205 cursor = buf; 206 while (*cursor != ' ' && cursor < reqEnd) { 207 cursor++; 208 } 209 210 cursor++; 211 if (cursor >= reqEnd || *cursor == ' ') { 212 puts("4 Path"); 213 send(mysock, "4 Path\r\n", sizeof("4 Path\r\n"), MSG_NOSIGNAL); 214 close(mysock); 215 continue; 216 } 217 218 path = cursor; 219 220 // get content_length 221 while (*cursor != ' ' && cursor < reqEnd) { 222 cursor++; 223 } 224 225 cursor++; 226 if (cursor >= reqEnd || *cursor == ' ') { 227 puts("4 Length"); 228 send(mysock, "4 Length\r\n", sizeof("4 Length\r\n"), MSG_NOSIGNAL); 229 close(mysock); 230 continue; 231 } 232 233 content_length = cursor; 234 while(cursor < reqEnd) { 235 if (!isdigit(*cursor)) { 236 isBad = true; 237 break; 238 } 239 cursor++; 240 } 241 242 // the request must not have any content 243 // content_length = 0 244 if (isBad || reqEnd - content_length > 1 || *content_length != '0') { 245 puts("4 Length"); 246 send(mysock, "4 Length\r\n", sizeof("4 Length\r\n"), MSG_NOSIGNAL); 247 close(mysock); 248 continue; 249 } 250 251 // replace SPC with 0 at the end of path 252 *(content_length-1) = 0; 253 254 // build server path 255 char localPath[PATH_MAX] = {0}; 256 if (rootLen + strlen(path) >= PATH_MAX) { 257 puts("5 Path too long"); 258 send(mysock, "5 Path too long\r\n", sizeof("5 Path too long\r\n"), MSG_NOSIGNAL); 259 close(mysock); 260 continue; 261 } 262 memcpy(localPath, root, rootLen); 263 cursor = localPath + rootLen; 264 memcpy(cursor, path, strlen(path)); 265 266 // check path 267 if (access(localPath, R_OK) == -1) { 268 puts("4 Not found"); 269 send(mysock, "4 Not found\r\n", sizeof("4 Not found\r\n"), MSG_NOSIGNAL); 270 close(mysock); 271 continue; 272 } 273 char realPath[PATH_MAX] = {0}; 274 realpath(localPath, realPath); 275 if (memcmp(realPath, root, rootLen-slash) != 0) { 276 puts("4 Not found"); 277 send(mysock, "4 Not found\r\n", sizeof("4 Not found\r\n"), MSG_NOSIGNAL); 278 close(mysock); 279 continue; 280 } 281 282 size_t pathLen = strlen(realPath); 283 cursor = realPath + pathLen; 284 if (isDir(realPath)) { 285 if (pathLen + strlen("/index.gmi") >= PATH_MAX) { 286 puts("5 Path too long"); 287 send(mysock, "5 Path too long\r\n", sizeof("5 Path too long\r\n"), MSG_NOSIGNAL); 288 close(mysock); 289 continue; 290 } 291 memcpy(cursor, "/index.gmi", strlen("/index.gmi")); 292 cursor += strlen("/index.gmi"); 293 } 294 295 FILE *f = fopen(realPath, "r"); 296 if (!f) { 297 puts("4 Page not found"); 298 send(mysock, "4 Page not found\r\n", sizeof("4 Page not found\r\n"), MSG_NOSIGNAL); 299 close(mysock); 300 continue; 301 } 302 303 // request in buf is not needed anymore, reuse buf for response 304 305 // check gemini extension 306 char *mimetype = "application/octet-stream"; 307 if (strlen(realPath) > 4 && memcmp(cursor-2, "md", 2) == 0) { 308 mimetype = "text/markdown"; 309 } 310 else if (strlen(realPath) > 5 && memcmp(cursor-3, "gmi", 3) == 0) { 311 mimetype = "text/gemini"; 312 } 313 else if (strlen(realPath) > 10 && memcmp(cursor-2, "markdown", 8) == 0) { 314 mimetype = "text/markdown"; 315 } 316 317 int len = sprintf(buf, "2 %s\r\n", mimetype); 318 if (send(mysock, buf, len, MSG_NOSIGNAL) != -1) { 319 // print response in terminal 320 // remove \r\n 321 buf[len-2] = 0; 322 puts(buf); 323 324 // send file 325 size_t fsz; 326 while (fsz = fread(buf, 1, (size_t)sizeof(buf) , f)) { 327 ssize_t r = send(mysock, buf, fsz, MSG_NOSIGNAL); 328 if (r == -1) { 329 perror("write failed"); 330 puts("closed socket"); 331 break; 332 } 333 } 334 } 335 else { 336 perror("write failed"); 337 } 338 fclose(f); 339 close(mysock); 340 } 341 } while(1); 342 343 puts("Server stopped."); 344 }