💾 Archived View for gmi.noulin.net › gitRepositories › spartserv › file › sparline.c.gmi captured on 2023-01-29 at 13:30:25. Gemini links have been rewritten to link to archived content
-=-=-=-=-=-=-
sparline.c (20841B)
1 // Usage: sparline spartan://hostname:port/path --infile filename 2 // Prompt: Enter link number, b, - or + for back and q for quit 3 4 #define _GNU_SOURCE 5 #include <stdio.h> 6 #include <sys/types.h> 7 #include <sys/socket.h> 8 #include <netdb.h> 9 #include <netinet/in.h> 10 #include <string.h> 11 #include <stdlib.h> 12 #include <ctype.h> 13 #include <unistd.h> 14 #include <sys/stat.h> 15 #include <sys/time.h> 16 #include <stdbool.h> 17 18 // links in page 19 // elements are offsets in page 20 // [][0] is link start 21 // [][1] is link end 22 // linkCount is the number of links on the page 23 size_t links[100000][2] = {0}; 24 size_t linkCount = 0; 25 char *page = NULL; 26 size_t pageBufSize = 0; 27 28 // history is a dynamic vector of historyEt element 29 // it is used as a stack 30 // pop is free string and decrease vector count 31 typedef struct { 32 char *hostname; 33 char *port; 34 char *path; 35 char *link; 36 } historyEt; 37 38 #define sliceT(typeName, elementType)\ 39 typedef struct {\ 40 size_t count;\ 41 elementType *array;\ 42 } typeName 43 44 sliceT(historyt, historyEt); 45 46 #define var __auto_type 47 #define TOKENPASTE2(a, b) a ## b 48 #define TOKENPASTE(a, b) TOKENPASTE2(a, b) 49 #define UNIQVAR(name) TOKENPASTE(name, __LINE__) 50 51 #define sliceInitCount(name, countInt) do{\ 52 var UNIQVAR(c) = countInt;\ 53 (name)->array = malloc(UNIQVAR(c) * sizeof (name)->array[0]);\ 54 (name)->count = 0;\ 55 } while(0) 56 57 #define sliceSz 1 58 59 #define sliceAlloc(name) do{\ 60 if (!(name)->array) {\ 61 (name)->array = malloc(sliceSz * sizeof (name)->array[0]);\ 62 }\ 63 else {\ 64 (name)->array = realloc((name)->array, ((name)->count + sliceSz) * sizeof (name)->array[0]);\ 65 }\ 66 } while(0) 67 68 #define slicePush(name) do {\ 69 sliceAlloc(name);\ 70 (name)->count++;\ 71 } while(0) 72 73 #define sliceAt(name, index) ((name)->array[index]) 74 #define sliceLast(name) ((name)->array[(name)->count-1]) 75 76 #define sliceAppend(name, v) do{\ 77 slicePush(name);\ 78 sliceLast(name) = v;\ 79 } while(0) 80 81 /** 82 * convert string to decimal integer 83 * 84 * \param 85 * string 86 * \return 87 * int64_t 88 * 0 when string represents 0 or doesnt represent a number or the input is NULL 89 */ 90 int64_t parseInt(const char *string) { 91 while (!isdigit(*string) && *string != '-' && *string != 0) { 92 string++; 93 } 94 int64_t r = strtoll(string, NULL, 10); 95 return(r); 96 } 97 98 #define startMax 20 99 100 /** 101 * read String 102 * read user input (one line) as a string 103 * 104 * there is no size limit and the buffer expands as needed 105 * 106 * \return 107 * line from the user (you must free the pointer) 108 * NULL when buffer allocation failed 109 */ 110 char *readS(void) { 111 int max = startMax; 112 113 char *s = malloc((size_t)max); 114 if (!s) { 115 return(NULL); 116 } 117 118 int i = 0; 119 while (1) { 120 int c = getchar(); 121 if (c == '\n') { 122 s[i] = 0; 123 break; 124 } 125 s[i] = (char)c; 126 if (i == max-1) { 127 // buffer full 128 max += max; 129 char *tmp = realloc(s, (size_t)max); 130 if (!tmp) { 131 free(s); 132 return(NULL); 133 } 134 s = tmp; 135 } 136 i++; 137 } 138 return(s); 139 } 140 141 // makeRoom is dynamic memory allocation algorithm 142 // given a length, an allocated size and the additionnal length, 143 // makeRoom returns the new allocated size for realloc 144 // when the new allocated size equals alloc value, there is no need to realloc the memory, enough space is already available 145 #define prealloc (1024*1024) 146 #define funcbegin ({ 147 #define funcend }) 148 #define makeRoom(length, alloc, addlength) funcbegin\ 149 typeof(alloc) r;\ 150 typeof(alloc) newlen = (length) + (addlength);\ 151 if (newlen < (alloc)) {\ 152 r = alloc;\ 153 } \ 154 else {\ 155 if (newlen < prealloc) {\ 156 r = newlen * 2;\ 157 }\ 158 else {\ 159 r = newlen + prealloc;\ 160 }\ 161 }\ 162 r;\ 163 funcend 164 165 bool getPage(char *hostname, char *ports, char *path, size_t content_length, void *senddata) { 166 int sock; 167 struct sockaddr_in server; 168 struct hostent *hp; 169 int mysock; 170 char buf[4096] = {0}; 171 char redirectPath[4096] = {0}; 172 int rval; 173 int i; 174 bool r = true; 175 176 openSocket: 177 sock = socket(AF_INET, SOCK_STREAM, 0); 178 if (sock < 0){ 179 perror("Failed to create socket"); 180 r = false; 181 goto showPage; 182 } 183 184 // Set 10s timeouts for receive and send (SO_RCVTIMEO and SO_SNDTIMEO) 185 struct timeval timeout; 186 timeout.tv_sec = 10; 187 timeout.tv_usec = 0; 188 if (setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (void *) &timeout, sizeof(timeout)) < 0) { 189 perror("receive timeout failed"); 190 close(sock); 191 r = false; 192 goto showPage; 193 } 194 if (setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, (void *) &timeout, sizeof(timeout)) < 0) { 195 perror("send timeout failed"); 196 close(sock); 197 r = false; 198 goto showPage; 199 } 200 201 server.sin_family = AF_INET; 202 203 hp = gethostbyname(hostname); 204 if (hp==0) { 205 perror("gethostbyname failed"); 206 close(sock); 207 r = false; 208 goto showPage; 209 } 210 211 memcpy(&server.sin_addr, hp->h_addr, hp->h_length); 212 213 int64_t port = parseInt(ports); 214 215 if (port < 1 || port > 65000) { 216 close(sock); 217 printf("Invalid port %d.\n", port); 218 r = false; 219 goto showPage; 220 } 221 222 server.sin_port = htons(port); 223 224 if (connect(sock,(struct sockaddr *) &server, sizeof(server))){ 225 perror("connect failed"); 226 close(sock); 227 r = false; 228 goto showPage; 229 } 230 231 // build request 232 // hostname SPC path SPC content_length\r\n 233 size_t len = strlen(hostname); 234 memcpy(buf, hostname, len); 235 buf[len] = ' '; 236 char *cursor = buf + len + 1; 237 len = strlen(path); 238 memcpy(cursor, path, len); 239 char lenstr[50]; 240 int ln = sprintf(lenstr, " %d\r\n", content_length); 241 memcpy(cursor + len, lenstr, ln); 242 cursor += len + ln; 243 244 // show request 245 *cursor = 0; 246 puts(buf); 247 printf("Page URL: spartan://%s:%s%s\n", hostname, ports, path); 248 249 // send request 250 if(send(sock, buf, cursor - buf, 0) < 0){ 251 perror("send failed"); 252 close(sock); 253 r = false; 254 goto showPage; 255 } 256 257 if (content_length > 0) { 258 // send data with request 259 size_t offset = 0; 260 while (content_length) { 261 size_t tosend = content_length > 2048 ? 2048 : content_length; 262 if(send(sock, senddata + offset, tosend, 0) < 0){ 263 perror("send failed"); 264 close(sock); 265 r = false; 266 goto showPage; 267 } 268 content_length -= tosend; 269 offset += tosend; 270 } 271 } 272 273 // receive server response 274 size_t pageSize = sizeof(buf); 275 size_t offset = 0; 276 free(page); 277 page = malloc(pageSize); 278 page[0] = 0; 279 do { 280 rval = recv(sock, buf, sizeof(buf), 0); 281 if (rval != -1 && rval != 0) { 282 size_t newSize = makeRoom(offset, pageSize, rval); 283 if (newSize > pageSize) { 284 char *tmp = realloc(page, newSize); 285 if (!tmp) { 286 // memory allocation error 287 // crash 288 exit(1); 289 } 290 page = tmp; 291 pageSize = newSize; 292 } 293 memcpy(page+offset, buf, rval); 294 offset += rval; 295 } 296 } while (rval != -1 && rval != 0); 297 close(sock); 298 299 if (buf[0] == '3' && buf[1] == ' ' && buf[2] != ' ' && offset <= sizeof(redirectPath)) { 300 // redirect 301 // scan path and reopen socket 302 cursor = buf + 2; 303 while (cursor < buf + offset && 304 !isspace(*cursor)) { 305 cursor++; 306 } 307 *cursor = 0; 308 puts(buf); 309 memcpy(redirectPath, buf+2, cursor - buf - 2); 310 redirectPath[cursor - buf - 2] = 0; 311 path = redirectPath; 312 goto openSocket; 313 } 314 315 // save page size for reuse 316 // when a link fails to load, the current page is reused 317 pageBufSize = offset; 318 319 // show page 320 // highlight headers, lists, blockquote, link and fixedwidth blocks 321 // collect links in links array 322 showPage: 323 if (page == NULL) return false; 324 linkCount = 0; 325 enum {normal, header, link, list, fixedwidth, blockquote}; 326 int state = normal; 327 #define RST "\x1B[0m" 328 #define BLD "\x1B[1m" 329 #define RED "\x1B[31m" 330 #define GRN "\x1B[32m" 331 #define YLW "\x1B[33m" 332 #define BLU "\x1B[34m" 333 #define MGT "\x1B[35m" 334 #define CYN "\x1B[36m" 335 #define WHT "\x1B[37m" 336 puts(RED"──────────────────────────────────────────────────────────────────────"RST); 337 for (int i = 0; i < pageBufSize; i++) { 338 if (state != fixedwidth) { 339 if (page[i] == '#' && (i == 0 || page[i-1] == '\n')) { 340 state = header; 341 printf(BLD YLW); 342 } 343 if (page[i] == '*' && (i == 0 || page[i-1] == '\n')) { 344 state = list; 345 printf(CYN); 346 } 347 if (page[i] == '>' && (i == 0 || page[i-1] == '\n')) { 348 state = blockquote; 349 printf(BLD WHT); 350 } 351 if (page[i] == '=' && page[i+1] == '>' && (i == 0 || page[i-1] == '\n')) { 352 state = link; 353 links[linkCount][0] = i; 354 links[linkCount][1] = 0; // invalid 355 printf(BLD GRN "%d " RST BLD BLU, linkCount); 356 } 357 } 358 if (page[i] == '`' && page[i+1] == '`' && page[i+2] == '`' && (i == 0 || page[i-1] == '\n')) { 359 if (state == normal) { 360 state = fixedwidth; 361 printf(BLD MGT); 362 } 363 else if (state == fixedwidth) { 364 state = normal; 365 printf(RST); 366 } 367 } 368 if (state != normal && state != fixedwidth && page[i] == '\n') { 369 if (state == link) { 370 links[linkCount++][1] = i; 371 } 372 state = normal; 373 printf(RST); 374 } 375 putchar(page[i]); 376 } 377 return true; 378 } 379 380 historyEt parseURL(char *url) { 381 size_t len = strlen(url); 382 383 char *cursor = strstr(url, "spartan://"); 384 if (!cursor) return (historyEt){0}; 385 386 char *s = cursor + strlen("spartan://"); 387 cursor = s; 388 // scan hostname 389 while(*cursor != '/' && 390 *cursor != ' ' && 391 *cursor != '\t' && 392 *cursor != ':' && 393 *cursor != '\n' && 394 *cursor != 0 && 395 cursor < url + len) { 396 cursor++; 397 } 398 char *hostname = malloc(cursor-s+1); 399 memcpy(hostname, s, cursor-s); 400 hostname[cursor-s] = 0; 401 402 char *port; 403 if (*cursor == ':') { 404 // port is specified 405 cursor++; 406 port = cursor; 407 while(isdigit(*cursor)) { 408 cursor++; 409 } 410 char c = *cursor; 411 *cursor = 0; 412 port = strdup(port); 413 *cursor = c; 414 } 415 else { 416 port = strdup("300"); 417 } 418 419 char *path; 420 if (*cursor == ' ' || 421 *cursor == '\t' || 422 *cursor == '\n' || 423 *cursor == 0) { 424 // path is empty 425 path = strdup("/"); 426 } 427 else { 428 // *cursor == '/' 429 // scan path 430 s = cursor; 431 while(!isspace(*cursor)) { 432 cursor++; 433 } 434 435 if (cursor == s+1) { 436 // path is / 437 path = strdup("/"); 438 } 439 else { 440 path = malloc(cursor-s+1); 441 memcpy(path, s, cursor-s); 442 path[cursor-s] = 0; 443 } 444 } 445 446 return (historyEt){.hostname = hostname, .port = port, .path = path}; 447 } 448 449 /** 450 * get file size 451 * 452 * \param 453 * filePath: path to file 454 * \return 455 * ssize_t >= 0 size 456 * -1 an error occured or filePath is NULL or empty string 457 */ 458 ssize_t fileSize(const char *filePath) { 459 struct stat st; 460 461 int r = stat(filePath, &st); 462 if (r) { 463 printf("Error, the path was: \"%s\"\n", filePath); 464 return(-1); 465 } 466 467 // macOS returns a varying number a number above the constant below 468 // when the file doesnt exists 469 if ((uint64_t)(st.st_size) > 140734000000000) { 470 return(-1);//LCOV_EXCL_LINE 471 } 472 return(st.st_size); 473 } 474 475 int main(int ac, char **av){ 476 if (ac < 2) { 477 puts("Usage: sparline spartan://hostname:port/path --infile filename\n" 478 "Default port is 300\n" 479 "Prompt: Enter link number, b, - or + for back and q for quit"); 480 return 0; 481 } 482 483 historyt history; 484 sliceInitCount(&history, 16); 485 486 historyEt e = parseURL(av[1]); 487 if (!e.hostname) { 488 puts("Error: Could not parse the url in argument 1"); 489 puts(av[1]); 490 return 1; 491 } 492 sliceAppend(&history, e); 493 494 if (ac > 3) { 495 // check for --infile 496 if (strcmp(av[2], "--infile") == 0) { 497 puts(av[3]); 498 ssize_t size = fileSize(av[3]); 499 if (size < 0) return 1; 500 char *data = malloc(size); 501 FILE *f = fopen(av[3], "r"); 502 size_t sz = fread(data, 1, size, f); 503 fclose(f); 504 if (sz < size) { 505 puts("Could not read complete file."); 506 return 1; 507 } 508 getPage(e.hostname, e.port, e.path, size /*content length*/, data /*send data*/); 509 free(data); 510 free(page); 511 return 0; 512 } 513 } 514 515 bool r = getPage(e.hostname, e.port, e.path, 0 /*content length*/, NULL /*send data*/); 516 if (!r) { 517 // get page failed 518 free(page); 519 return 1; 520 } 521 522 char *userInput = strdup(""); 523 while(strcmp(userInput, "q") != 0) { 524 // print prompt with link count on current page 525 if (!linkCount) 526 printf("> "); 527 else 528 printf("%d links > ", linkCount); 529 free(userInput); 530 // userInput is allocated in readS 531 userInput = readS(); 532 puts(userInput); 533 if (isdigit(userInput[0])) { 534 // go to link 535 int link = parseInt(userInput); 536 if (link >= linkCount) { 537 printf("Link number too high: %d, link count: %d\n", link, linkCount); 538 continue; 539 } 540 // build a valid link string 541 page[links[link][1]] = 0; 542 char *s = &page[links[link][0]]; 543 char *hostname; 544 char *port; 545 char *path; 546 547 char *cursor = strstr(s, "spartan://"); 548 if (!cursor) { 549 if (strstr(s, "://")) { 550 puts("Only spartan links are supported."); 551 continue; 552 } 553 // this is a path 554 // sometimes there is no space after => 555 cursor = isspace(*(s+2)) ? s+3 : s+2; 556 // p is link start 557 char *p = cursor; 558 // search for link end and skip the title 559 while (!isspace(*cursor) && cursor < &page[links[link][1]]) { 560 cursor++; 561 } 562 *cursor = 0; 563 // there are no hostname and port in link, use the previous ones 564 hostname = strdup(sliceLast(&history).hostname); 565 port = strdup(sliceLast(&history).port); 566 if (*p != '/') { 567 // relative path 568 // check if current path is a file like index.gmi 569 // to avoid creating a path like: 570 // /index.gmi/p 571 size_t len = strlen(sliceLast(&history).path); 572 if (len > 4 && memcmp(sliceLast(&history).path + len - 4, ".gmi", 4) == 0) { 573 // when path is .. remove filename only 574 if (strcmp(p, "..") == 0) 575 p = ""; 576 else if (strcmp(p, ".") == 0) { 577 // path is ., reload same page as previous one 578 path = strdup(sliceLast(&history).path); 579 goto downloadPage; 580 } 581 // previous has a gmi extension 582 // find dirname, can be empty string 583 char *hp = sliceLast(&history).path + len; 584 while(hp > sliceLast(&history).path) { 585 hp--; 586 if (*hp == '/') { 587 *hp = 0; 588 asprintf(&path, "%s/%s", sliceLast(&history).path, p); 589 *hp = '/'; 590 goto downloadPage; 591 } 592 } 593 // no slash found in history path 594 asprintf(&path, "/%s", sliceLast(&history).path, p); 595 } 596 else 597 // add path to previous path 598 asprintf(&path, "%s/%s", sliceLast(&history).path, p); 599 } 600 else { 601 // absolute path starting with / 602 path = strdup(p); 603 } 604 goto downloadPage; 605 } 606 607 // this part is the same as the parseURL function 608 s = cursor + strlen("spartan://"); 609 cursor = s; 610 // scan hostname 611 while(*cursor != '/' && 612 *cursor != ' ' && 613 *cursor != '\t' && 614 *cursor != ':' && 615 *cursor != '\n' && 616 *cursor != 0 && 617 cursor < &page[links[link][1]]) { 618 cursor++; 619 } 620 hostname = malloc(cursor-s+1); 621 memcpy(hostname, s, cursor-s); 622 hostname[cursor-s] = 0; 623 624 if (*cursor == ':') { 625 // port is specified 626 cursor++; 627 port = cursor; 628 while(isdigit(*cursor)) { 629 cursor++; 630 } 631 char c = *cursor; 632 *cursor = 0; 633 port = strdup(port); 634 *cursor = c; 635 } 636 else { 637 // default port is 300 638 port = strdup("300"); 639 } 640 641 if (*cursor == ' ' || 642 *cursor == '\t' || 643 *cursor == '\n' || 644 *cursor == 0) { 645 // path is empty 646 path = strdup("/"); 647 } 648 else { 649 // *cursor == '/' 650 // scan path 651 s = cursor; 652 while(!isspace(*cursor)) { 653 cursor++; 654 } 655 656 if (cursor == s+1) { 657 // path is / 658 path = strdup("/"); 659 } 660 else { 661 path = malloc(cursor-s+1); 662 memcpy(path, s, cursor-s); 663 path[cursor-s] = 0; 664 } 665 } 666 // ^^ this part is the same as the parseURL function ^^ 667 668 downloadPage: 669 printf(BLD GRN "%s" RST "\n", &page[links[link][0]]); 670 671 // store hostname, port, path and link in history 672 e = (historyEt){.hostname = hostname, .port = port, .path = path, .link = strdup(&page[links[link][0]])}; 673 if (getPage(hostname, port, path, 0 /*content length*/, NULL /*send data*/)) { 674 // success 675 sliceAppend(&history, e); 676 } 677 } 678 else if (userInput[0] == 'b' || userInput[0] == '-' || userInput[0] == '+') { 679 // go back in history 680 if (history.count < 2) { 681 puts("First page in history."); 682 continue; 683 } 684 else { 685 free(sliceLast(&history).hostname); 686 free(sliceLast(&history).port); 687 free(sliceLast(&history).path); 688 free(sliceLast(&history).link); 689 history.count--; 690 if (sliceLast(&history).link) 691 puts(sliceLast(&history).link); 692 getPage(sliceLast(&history).hostname, sliceLast(&history).port, sliceLast(&history).path, 0 /*content length*/, NULL /*send data*/); 693 } 694 } 695 else if (userInput[0] == 'z') { 696 // used for debug 697 puts("break"); 698 } 699 } 700 701 free(page); 702 for (int i = 0; i < history.count ; i++) { 703 free(sliceAt(&history, i).hostname); 704 free(sliceAt(&history, i).port); 705 free(sliceAt(&history, i).path); 706 free(sliceAt(&history, i).link); 707 } 708 } 709