spartserv

Simple client and server for the spartan protocol
git clone https://noulin.net/git/spartserv.git
Log | Files | Refs | README

sparline.c (20815B)


      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", 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