💾 Archived View for gmi.noulin.net › gitRepositories › spartserv › file › spartserv.c.gmi captured on 2024-08-18 at 18:52:55. Gemini links have been rewritten to link to archived content
⬅️ Previous capture (2023-07-10)
-=-=-=-=-=-=-
spartserv.c (10929B)
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 }