spartserv

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