cgo

[Fork] Gopher Client
git clone https://git.jojolepro.com/cgo.git
Log | Files | Refs | README | LICENSE

cgo.c (24653B)


      1 /*
      2  * cgo - a simple terminal based gopher client
      3  * Copyright (c) 2019 Sebastian Steinhauer <s.steinhauer@yahoo.de>
      4  *
      5  * Permission to use, copy, modify, and distribute this software for any
      6  * purpose with or without fee is hereby granted, provided that the above
      7  * copyright notice and this permission notice appear in all copies.
      8  *
      9  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
     10  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
     11  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
     12  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
     13  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
     14  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
     15  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
     16  */
     17 #include <sys/types.h>
     18 #include <sys/stat.h>
     19 #include <sys/wait.h>
     20 #include <sys/socket.h>
     21 #include <netdb.h>
     22 #include <fcntl.h>
     23 #include <unistd.h>
     24 #include <errno.h>
     25 #include <stdio.h>
     26 #include <stdlib.h>
     27 #include <string.h>
     28 #include <ctype.h>
     29 
     30 /* some "configuration" */
     31 #define START_URI           "gopher://gopher.floodgap.com:70"
     32 #define CMD_TEXT            "less"
     33 #define CMD_IMAGE           "display"
     34 #define CMD_BROWSER         "firefox"
     35 #define CMD_PLAYER          "mplayer"
     36 #define CMD_TELNET          "telnet"
     37 #define COLOR_PROMPT        "1;34"
     38 #define COLOR_SELECTOR      "1;32"
     39 #define HEAD_CHECK_LEN      5
     40 #define GLOBAL_CONFIG_FILE  "/etc/cgorc"
     41 #define LOCAL_CONFIG_FILE   "/.cgorc"
     42 #define NUM_BOOKMARKS       20
     43 #define VERBOSE             "true"
     44 
     45 /* some internal defines */
     46 #define KEY_RANGE   	(('z' - 'a') + 1)
     47 
     48 /* structs */
     49 typedef struct link_s link_t;
     50 struct link_s {
     51     link_t  *next;
     52     char    which;
     53     short   key;
     54     char    *host;
     55     char    *port;
     56     char    *selector;
     57 };
     58 
     59 typedef struct config_s config_t;
     60 struct config_s {
     61     char    start_uri[512];
     62     char    cmd_text[512];
     63     char    cmd_image[512];
     64     char    cmd_browser[512];
     65     char    cmd_player[512];
     66     char    color_prompt[512];
     67     char    color_selector[512];
     68     char    verbose[512];
     69 };
     70 
     71 char        tmpfilename[256];
     72 link_t      *links = NULL;
     73 link_t      *history = NULL;
     74 int         link_key;
     75 char        current_host[512], current_port[64], current_selector[1024];
     76 char        parsed_host[512], parsed_port[64], parsed_selector[1024];
     77 char        bookmarks[NUM_BOOKMARKS][512];
     78 config_t    config;
     79 
     80 /* function prototypes */
     81 int parse_uri(const char *uri);
     82 
     83 /* implementation */
     84 void usage()
     85 {
     86     fputs("usage: cgo [-v] [-H] [gopher URI]\n",
     87             stderr);
     88     exit(EXIT_SUCCESS);
     89 }
     90 
     91 void banner(FILE *f)
     92 {
     93     fputs("cgo 0.6.0  Copyright (c) 2019  Sebastian Steinhauer\n", f);
     94 }
     95 
     96 int check_option_true(const char *option)
     97 {
     98     return strcasecmp(option, "false") && strcasecmp(option, "off");
     99 }
    100 
    101 void parse_config_line(const char *line)
    102 {
    103     char    token[1024];
    104     char    bkey[128];
    105     char    *value = NULL;
    106     int     i, j;
    107 
    108     while (*line == ' ' || *line == '\t') line++;
    109     for (i = 0; *line && *line != ' ' && *line != '\t'; line++)
    110         if (i < sizeof(token) - 1) token[i++] = *line;
    111     token[i] = 0;
    112 
    113     if (! strcmp(token, "start_uri")) value = &config.start_uri[0];
    114     else if (! strcmp(token, "cmd_text")) value = &config.cmd_text[0];
    115     else if (! strcmp(token, "cmd_browser")) value = &config.cmd_browser[0];
    116     else if (! strcmp(token, "cmd_image")) value = &config.cmd_image[0];
    117     else if (! strcmp(token, "cmd_player")) value = &config.cmd_player[0];
    118     else if (! strcmp(token, "color_prompt")) value = &config.color_prompt[0];
    119     else if (! strcmp(token, "color_selector")) value = &config.color_selector[0];
    120     else if (! strcmp(token, "verbose")) value = &config.verbose[0];
    121     else {
    122         for (j = 0; j < NUM_BOOKMARKS; j++) {
    123             snprintf(bkey, sizeof(bkey), "bookmark%d", j+1);
    124             if (! strcmp(token, bkey)) {
    125                 value = &bookmarks[j][0];
    126                 break;
    127             }
    128         }
    129         if (! value) return;
    130     };
    131 
    132     while (*line == ' ' || *line == '\t') line++;
    133     for (i = 0; *line; line++)
    134         if (i < 512-1) value[i++] = *line;
    135     for (i--; i > 0 && (value[i] == ' ' || value[i] == '\t'); i--) ;
    136     value[++i] = 0;
    137 
    138 }
    139 
    140 void load_config(const char *filename)
    141 {
    142     FILE    *fp;
    143     int     ch, i;
    144     char    line[1024];
    145 
    146     fp = fopen(filename, "r");
    147     if (! fp) return;
    148 
    149     memset(line, 0, sizeof(line));
    150     i = 0;
    151     ch = fgetc(fp);
    152     while (1) {
    153         switch (ch) {
    154             case '#':
    155                 while (ch != '\n' && ch != -1)
    156                     ch = fgetc(fp);
    157                 break;
    158             case -1:
    159                 parse_config_line(line);
    160                 fclose(fp);
    161                 return;
    162             case '\r':
    163                 ch = fgetc(fp);
    164                 break;
    165             case '\n':
    166                 parse_config_line(line);
    167                 memset(line, 0, sizeof(line));
    168                 i = 0;
    169                 ch = fgetc(fp);
    170                 break;
    171             default:
    172                 if (i < sizeof(line) - 1)
    173                     line[i++] = ch;
    174                 ch = fgetc(fp);
    175                 break;
    176         }
    177     }
    178 }
    179 
    180 void init_config()
    181 {
    182     char        filename[1024];
    183     const char  *home;
    184     int         i;
    185 
    186     /* copy defaults */
    187     snprintf(config.start_uri, sizeof(config.start_uri), START_URI);
    188     snprintf(config.cmd_text, sizeof(config.cmd_text), "%s", CMD_TEXT);
    189     snprintf(config.cmd_image, sizeof(config.cmd_image), "%s", CMD_IMAGE);
    190     snprintf(config.cmd_browser, sizeof(config.cmd_browser), "%s", CMD_BROWSER);
    191     snprintf(config.cmd_player, sizeof(config.cmd_player), "%s", CMD_PLAYER);
    192     snprintf(config.color_prompt, sizeof(config.color_prompt), "%s", COLOR_PROMPT);
    193     snprintf(config.color_selector, sizeof(config.color_selector), "%s", COLOR_SELECTOR);
    194     snprintf(config.verbose, sizeof(config.verbose), "%s", VERBOSE);
    195     for (i = 0; i < NUM_BOOKMARKS; i++) bookmarks[i][0] = 0;
    196     /* read configs */
    197     load_config(GLOBAL_CONFIG_FILE);
    198     home = getenv("HOME");
    199     if (home) {
    200         snprintf(filename, sizeof(filename), "%s%s", home, LOCAL_CONFIG_FILE);
    201         load_config(filename);
    202     }
    203 }
    204 
    205 int dial(const char *host, const char *port, const char *selector)
    206 {
    207     struct addrinfo hints;
    208     struct addrinfo *res, *r;
    209     int             srv = -1, l;
    210     char            request[512];
    211 
    212     memset(&hints, 0, sizeof(hints));
    213     hints.ai_family = AF_UNSPEC;
    214     hints.ai_socktype = SOCK_STREAM;
    215     if (getaddrinfo(host, port, &hints, &res) != 0) {
    216         fprintf(stderr, "error: cannot resolve hostname '%s:%s': %s\n",
    217                 host, port, strerror(errno));
    218         return -1;
    219     }
    220     for (r = res; r; r = r->ai_next) {
    221         srv = socket(r->ai_family, r->ai_socktype, r->ai_protocol);
    222         if (srv == -1)
    223             continue;
    224         if (connect(srv, r->ai_addr, r->ai_addrlen) == 0)
    225             break;
    226         close(srv);
    227     }
    228     freeaddrinfo(res);
    229     if (! r) {
    230         fprintf(stderr, "error: cannot connect to host '%s:%s'\n",
    231                 host, port);
    232         return -1;
    233     }
    234     snprintf(request, sizeof(request), "%s\r\n", selector);
    235     l = strlen(request);
    236     if (write(srv, request, l) != l) {
    237         fprintf(stderr, "error: cannot complete request\n");
    238         close(srv);
    239         return -1;
    240     }
    241     return srv;
    242 }
    243 
    244 int read_line(int fd, char *buf, size_t buf_len)
    245 {
    246     size_t  i = 0;
    247     char    c = 0;
    248 
    249     do {
    250         if (read(fd, &c, sizeof(char)) != sizeof(char))
    251             return 0;
    252         if (c != '\r')
    253             buf[i++] = c;
    254     } while (c != '\n' && i < buf_len);
    255     buf[i - 1] = '\0';
    256     return 1;
    257 }
    258 
    259 int download_file(const char *host, const char *port,
    260         const char *selector, int fd)
    261 {
    262     int             srvfd, len;
    263     unsigned long   total = 0;
    264     char            buffer[4096];
    265 
    266     if (check_option_true(config.verbose))
    267         printf("downloading [%s]...\r", selector);
    268     srvfd = dial(host, port, selector);
    269     if (srvfd == -1) {
    270         printf("\033[2Kerror: downloading [%s] failed\n", selector);
    271         close(fd);
    272         return 0;
    273     }
    274     while ((len = read(srvfd, buffer, sizeof(buffer))) > 0) {
    275         write(fd, buffer, len);
    276         total += len;
    277         if (check_option_true(config.verbose))
    278             printf("downloading [%s] (%ld kb)...\r", selector, total / 1024);
    279     }
    280     close(fd);
    281     close(srvfd);
    282     if (check_option_true(config.verbose))
    283         printf("\033[2Kdownloading [%s] complete\n", selector);
    284     return 1;
    285 }
    286 
    287 int download_temp(const char *host, const char *port, const char *selector)
    288 {
    289     int     tmpfd;
    290 
    291 #if defined(__OpenBSD__)
    292     strlcpy(tmpfilename, "/tmp/cgoXXXXXX", sizeof(tmpfilename));
    293 #else
    294     strcpy(tmpfilename, "/tmp/cgoXXXXXX");
    295 #endif
    296     tmpfd = mkstemp(tmpfilename);
    297     if (tmpfd == -1) {
    298         fputs("error: unable to create tmp file\n", stderr);
    299         return 0;
    300     }
    301     if (! download_file(host, port, selector, tmpfd)) {
    302         unlink(tmpfilename);
    303         return 0;
    304     }
    305     return 1;
    306 }
    307 
    308 int make_key(char c1, char c2, char c3)
    309 {
    310     if (! c1 || ! c2)
    311         return -1;
    312 
    313     if (! c3)
    314         return ((c1 - 'a') * KEY_RANGE) + (c2 - 'a');
    315     else
    316         return (((c1 - 'a' + 1) * KEY_RANGE * KEY_RANGE) + ((c2 - 'a') * KEY_RANGE) + (c3 - 'a'));
    317 }
    318 
    319 void make_key_str(int key, char *c1, char *c2, char *c3) {
    320     if (key < (KEY_RANGE * KEY_RANGE)) {
    321         *c1 = 'a' + (key / KEY_RANGE);
    322         *c2 = 'a' + (key % KEY_RANGE);
    323         *c3 = 0;
    324     } else {
    325         *c1 = 'a' + (key / (KEY_RANGE * KEY_RANGE)) - 1;
    326         *c2 = 'a' + ((key / KEY_RANGE) % KEY_RANGE);
    327         *c3 = 'a' + (key % KEY_RANGE);
    328     }
    329 }
    330 
    331 void add_link(char which, const char *name,
    332         const char *host, const char *port, const char *selector)
    333 {
    334     link_t  *link;
    335     char    a = 0, b = 0, c = 0;
    336 
    337     if (! host || ! port || ! selector)
    338         return; /* ignore incomplete selectors */
    339     link = calloc(1, sizeof(link_t));
    340     link->which = which;
    341     link->key = link_key;
    342     link->host = strdup(host);
    343     link->port = strdup(port);
    344     link->selector = strdup(selector);
    345     if (! links)
    346         link->next = NULL;
    347     else
    348         link->next = links;
    349     links = link;
    350 
    351     make_key_str(link_key++, &a, &b, &c);
    352     printf("\033[%sm%c%c%c\033[0m \033[1m%s\033[0m\n",
    353             config.color_selector, a, b, c, name);
    354 }
    355 
    356 void clear_links()
    357 {
    358     link_t  *link, *next;
    359 
    360     for (link = links; link; ) {
    361         next = link->next;
    362         free(link->host);
    363         free(link->port);
    364         free(link->selector);
    365         free(link);
    366         link = next;
    367     }
    368     links = NULL;
    369     link_key = 0;
    370 }
    371 
    372 void add_history()
    373 {
    374     link_t  *link;
    375 
    376     link = calloc(1, sizeof(link_t));
    377     link->host = strdup(current_host);
    378     link->port = strdup(current_port);
    379     link->selector = strdup(current_selector);
    380     link->which = 0;    /* not needed for history...just clear them */
    381     link->key = 0;
    382     if (! history)
    383         link->next = NULL;
    384     else
    385         link->next = history;
    386     history = link;
    387 }
    388 
    389 void handle_directory_line(char *line)
    390 {
    391     int     i;
    392     char    *lp, *last, *fields[4];
    393 
    394     /* tokenize */
    395     for (i = 0; i < 4; i++)
    396         fields[i] = NULL;
    397     last = &line[1];
    398     for (lp = last, i = 0; i < 4; lp++) {
    399         if (*lp == '\t' || *lp == '\0') {
    400             fields[i] = last;
    401             last = lp + 1;
    402             if (*lp == '\0')
    403                 break;
    404             *lp = '\0';
    405             i++;
    406         }
    407     }
    408     /* determine listing type */
    409     switch (line[0]) {
    410         case 'i':
    411         case '3':
    412             printf("   %s\n", fields[0]);
    413             break;
    414         case '.':   /* some gopher servers use this */
    415             puts("");
    416             break;
    417         case '0':
    418         case '1':
    419         case '5':
    420         case '7':
    421         case '8':
    422         case '9':
    423         case 'g':
    424         case 'I':
    425         case 'p':
    426         case 'h':
    427         case 's':
    428             add_link(line[0], fields[0], fields[2], fields[3], fields[1]);
    429             break;
    430         default:
    431             printf("miss [%c]: %s\n", line[0], fields[0]);
    432             break;
    433     }
    434 }
    435 
    436 int is_valid_directory_entry(const char *line)
    437 {
    438     switch (line[0]) {
    439         case 'i':
    440         case '3':
    441         case '.':   /* some gopher servers use this */
    442         case '0':
    443         case '1':
    444         case '5':
    445         case '7':
    446         case '8':
    447         case '9':
    448         case 'g':
    449         case 'I':
    450         case 'p':
    451         case 'h':
    452         case 's':
    453             return 1;
    454         default:
    455             return 0;
    456     }
    457 }
    458 
    459 void view_directory(const char *host, const char *port,
    460         const char *selector, int make_current)
    461 {
    462     int     is_dir;
    463     int     srvfd, i, head_read;
    464     char    line[1024];
    465     char    head[HEAD_CHECK_LEN][1024];
    466 
    467     srvfd = dial(host, port, selector);
    468     if (srvfd != -1) {  /* only adapt current prompt when successful */
    469         /* make history entry */
    470         if (make_current)
    471             add_history();
    472         /* don't overwrite the current_* things... */
    473         if (host != current_host)
    474             snprintf(current_host, sizeof(current_host), "%s", host);
    475         if (port != current_port)
    476             snprintf(current_port, sizeof(current_port), "%s", port);
    477         if (selector != current_selector)
    478             snprintf(current_selector, sizeof(current_selector),
    479                     "%s", selector);
    480     }
    481     clear_links();  /* clear links *AFTER* dialing out!! */
    482     if (srvfd == -1)
    483         return; /* quit if not successful */
    484     head_read = 0;
    485     is_dir = 1;
    486     while (head_read < HEAD_CHECK_LEN && read_line(srvfd, line, sizeof(line))) {
    487         strcpy(head[head_read], line);
    488         if (!is_valid_directory_entry(head[head_read])) {
    489             is_dir = 0;
    490             break;
    491         }
    492         head_read++;
    493     }
    494     if (!is_dir) {
    495         puts("error: Not a directory.");
    496         close(srvfd);
    497         return;
    498     }
    499     for (i = 0; i < head_read; i++) {
    500         handle_directory_line(head[i]);
    501     }
    502     while (read_line(srvfd, line, sizeof(line))) {
    503         handle_directory_line(line);
    504     }
    505     close(srvfd);
    506 }
    507 
    508 void view_file(const char *cmd, const char *host,
    509         const char *port, const char *selector)
    510 {
    511     pid_t   pid;
    512     int     status, i, j;
    513     char    buffer[1024], *argv[32], *p;
    514 
    515     if (check_option_true(config.verbose))
    516         printf("h(%s) p(%s) s(%s)\n", host, port, selector);
    517 
    518     if (! download_temp(host, port, selector))
    519         return;
    520 
    521     /* parsed command line string */
    522     argv[0] = &buffer[0];
    523     for (p = (char*) cmd, i = 0, j = 1; *p && i < sizeof(buffer) - 1 && j < 30; ) {
    524         if (*p == ' ' || *p == '\t') {
    525             buffer[i++] = 0;
    526             argv[j++] = &buffer[i];
    527             while (*p == ' ' || *p == '\t') p++;
    528         } else buffer[i++] = *p++;
    529     }
    530     buffer[i] = 0;
    531     argv[j++] = tmpfilename;
    532     argv[j] = NULL;
    533 
    534     /* fork and execute */
    535     if (check_option_true(config.verbose))
    536         printf("executing: %s %s\n", cmd, tmpfilename);
    537     pid = fork();
    538     if (pid == 0) {
    539         if (execvp(argv[0], argv) == -1)
    540             puts("error: execvp() failed!");
    541     } else if (pid == -1) puts("error: fork() failed");
    542     sleep(1); /* to wait for browsers etc. that return immediatly */
    543     waitpid(pid, &status, 0);
    544     unlink(tmpfilename);
    545 }
    546 
    547 void view_telnet(const char *host, const char *port)
    548 {
    549     pid_t   pid;
    550     int     status;
    551 
    552     printf("executing: %s %s %s\n", CMD_TELNET, host, port);
    553     pid = fork();
    554     if (pid == 0) {
    555         if (execlp(CMD_TELNET, CMD_TELNET, host, port, NULL) == -1)
    556             puts("error: execlp() failed!");
    557     } else if (pid == -1) puts("error: fork() failed!");
    558     waitpid(pid, &status, 0);
    559     puts("(done)");
    560 }
    561 
    562 void view_download(const char *host, const char *port, const char *selector)
    563 {
    564     int     fd;
    565     char    filename[1024], line[1024];
    566 
    567     snprintf(filename, sizeof(filename), "%s", strrchr(selector, '/') + 1);
    568     printf("enter filename for download [%s]: ", filename);
    569     fflush(stdout);
    570     if (! read_line(0, line, sizeof(line))) {
    571         puts("download aborted");
    572         return;
    573     }
    574     if (strlen(line) > 0)
    575 #if defined(__OpenBSD__)
    576         strlcpy(filename, line, sizeof(filename));
    577 #else
    578         strcpy(filename, line);
    579 #endif
    580     fd = open(filename, O_CREAT | O_WRONLY, S_IRUSR | S_IWUSR);
    581     if (fd == -1) {
    582         printf("error: unable to create file [%s]: %s\n",
    583                 filename, strerror(errno));
    584         return;
    585     }
    586     if (! download_file(host, port, selector, fd)) {
    587         printf("error: unable to download [%s]\n", selector);
    588         unlink(filename);
    589         return;
    590     }
    591 }
    592 
    593 void view_search(const char *host, const char *port, const char *selector)
    594 {
    595     char    search_selector[1024];
    596     char    line[1024];
    597 
    598     printf("enter search string: ");
    599     fflush(stdout);
    600     if (! read_line(0, line, sizeof(line))) {
    601         puts("search aborted");
    602         return;
    603     }
    604     snprintf(search_selector, sizeof(search_selector), "%s\t%s",
    605             selector, line);
    606     view_directory(host, port, search_selector, 1);
    607 }
    608 
    609 void view_history(int key)
    610 {
    611     int     history_key = 0;
    612     char    a, b, c;
    613     link_t  *link;
    614 
    615     if (! history) {
    616         puts("(empty history)");
    617         return;
    618     }
    619     if ( key < 0 ) {
    620         puts("(history)");
    621         for ( link = history; link; link = link->next ) {
    622             make_key_str(history_key++, &a, &b, &c);
    623             printf("\033[%sm%c%c%c\033[0m \033[1m%s:%s%s\033[0m\n",
    624                 COLOR_SELECTOR, a, b, c, link->host, link->port, link->selector);
    625         }
    626     } else {
    627         /* traverse history list */
    628         for ( link = history; link; link = link->next, ++history_key ) {
    629             if ( history_key == key ) {
    630                 view_directory(link->host, link->port, link->selector, 0);
    631                 return;
    632             }
    633         }
    634         puts("history item not found");
    635     }
    636 }
    637 
    638 void view_bookmarks(int key)
    639 {
    640     int     i;
    641     char    a, b, c;
    642 
    643     if (key < 0) {
    644         puts("(bookmarks)");
    645         for (i = 0; i < NUM_BOOKMARKS; i++) {
    646             if (bookmarks[i][0]) {
    647                 make_key_str(i, &a, &b, &c);
    648                 printf("\033[%sm%c%c%c\033[0m \033[1m%s\033[0m\n",
    649                     COLOR_SELECTOR, a, b, c, &bookmarks[i][0]);
    650             }
    651         }
    652     } else {
    653         for (i = 0; i < NUM_BOOKMARKS; i++) {
    654             if (bookmarks[i][0] && i == key) {
    655                 if (parse_uri(&bookmarks[i][0])) view_directory(parsed_host, parsed_port, parsed_selector, 0);
    656                 else printf("invalid gopher URI: %s", &bookmarks[i][0]);
    657                 return;
    658             }
    659         }
    660     }
    661 }
    662 
    663 void pop_history()
    664 {
    665     link_t  *next;
    666 
    667     if (! history) {
    668         puts("(empty history)");
    669         return;
    670     }
    671     /* reload page from history (and don't count as history) */
    672     view_directory(history->host, history->port, history->selector, 0);
    673     /* history is history... :) */
    674     next = history->next;
    675     free(history->host);
    676     free(history->port);
    677     free(history->selector);
    678     free(history);
    679     history = next;
    680 }
    681 
    682 int follow_link(int key)
    683 {
    684     link_t  *link;
    685 
    686     for (link = links; link; link = link->next) {
    687         if (link->key != key)
    688             continue;
    689         switch (link->which) {
    690             case '0':
    691                 view_file(&config.cmd_text[0], link->host, link->port, link->selector);
    692                 break;
    693             case '1':
    694                 view_directory(link->host, link->port, link->selector, 1);
    695                 break;
    696             case '7':
    697                 view_search(link->host, link->port, link->selector);
    698                 break;
    699             case '5':
    700             case '9':
    701                 view_download(link->host, link->port, link->selector);
    702                 break;
    703             case '8':
    704                 view_telnet(link->host, link->port);
    705                 break;
    706             case 'g':
    707             case 'I':
    708             case 'p':
    709                 view_file(&config.cmd_image[0], link->host, link->port, link->selector);
    710                 break;
    711             case 'h':
    712                 view_file(&config.cmd_browser[0], link->host, link->port, link->selector);
    713                 break;
    714             case 's':
    715                 view_file(&config.cmd_player[0], link->host, link->port, link->selector);
    716                 break;
    717             default:
    718                 printf("missing handler [%c]\n", link->which);
    719                 break;
    720         }
    721         return 1; /* return the array is broken after view! */
    722     }
    723     return 0;
    724 }
    725 
    726 void download_link(int key)
    727 {
    728     link_t  *link;
    729 
    730     for (link = links; link; link = link->next) {
    731         if (link->key != key)
    732             continue;
    733         view_download(link->host, link->port, link->selector);
    734         return;
    735     }
    736     puts("link not found");
    737 }
    738 
    739 int parse_uri(const char *uri)
    740 {
    741     int     i;
    742 
    743     /* strip gopher:// */
    744     if (! strncmp(uri, "gopher://", 9))
    745         uri += 9;
    746     /* parse host */
    747     for (i = 0; *uri && *uri != ':' && *uri != '/'; uri++) {
    748         if (*uri != ' ' && i < sizeof(parsed_host) - 1)
    749             parsed_host[i++] = *uri;
    750     }
    751     if (i > 0) parsed_host[i] = 0;
    752     else return 0;
    753     /* parse port */
    754     if (*uri == ':') {
    755         uri++;
    756         for (i = 0; *uri && *uri != '/'; uri++)
    757             if (*uri != ' ' && i < sizeof(parsed_port) - 1)
    758                 parsed_port[i++] = *uri;
    759         parsed_port[i] = 0;
    760     } else snprintf(parsed_port, sizeof(parsed_port), "%d", 70);
    761     /* parse selector */
    762     if (*uri == '/') {
    763         for (i = 0, ++uri; *uri; ++uri)
    764             if (i < sizeof(parsed_selector) - 1)
    765                 parsed_selector[i++] = *uri;
    766         parsed_selector[i++] = *uri;
    767     } else snprintf(parsed_selector, sizeof(parsed_selector), "%s", "");
    768 
    769     return 1;
    770 }
    771 
    772 int main(int argc, char *argv[])
    773 {
    774     int     i;
    775     char    line[1024], *uri;
    776 
    777     /* copy defaults */
    778     init_config();
    779     uri = &config.start_uri[0];
    780 
    781     /* parse command line */
    782     for (i = 1; i < argc; i++) {
    783         if (argv[i][0] == '-') switch(argv[i][1]) {
    784             case 'H':
    785                 usage();
    786                 break;
    787             case 'v':
    788                 banner(stdout);
    789                 exit(EXIT_SUCCESS);
    790             default:
    791                 usage();
    792         } else {
    793             uri = argv[i];
    794         }
    795     }
    796 
    797     /* parse uri */
    798     if (! parse_uri(uri)) {
    799         banner(stderr);
    800         fprintf(stderr, "invalid gopher URI: %s", argv[i]);
    801         exit(EXIT_FAILURE);
    802     }
    803 
    804     /* main loop */
    805     view_directory(parsed_host, parsed_port, parsed_selector, 0);
    806     for (;;) {
    807         printf("\033[%sm%s:%s%s\033[0m ", config.color_prompt,
    808                 current_host, current_port, current_selector);
    809         fflush(stdout); /* to display the prompt */
    810         if (! read_line(0, line, sizeof(line))) {
    811             puts("QUIT");
    812             return EXIT_SUCCESS;
    813         }
    814         i = strlen(line);
    815         switch (line[0]) {
    816             case '?':
    817                 puts(
    818                     "?          - help\n"
    819                     "*          - reload directory\n"
    820                     "<          - go back in history\n"
    821                     ".[LINK]    - download the given link\n"
    822                     "H          - show history\n"
    823                     "H[LINK]    - jump to the specified history item\n"
    824                     "G[URI]     - jump to the given gopher URI\n"
    825                     "B          - show bookmarks\n"
    826                     "B[LINK]    - jump to the specified bookmark item\n"
    827                     "C^d        - quit");
    828                 break;
    829             case '<':
    830                 pop_history();
    831                 break;
    832             case '*':
    833                 view_directory(current_host, current_port,
    834                         current_selector, 0);
    835                 break;
    836             case '.':
    837                 download_link(make_key(line[1], line[2], line[3]));
    838                 break;
    839             case 'H':
    840                 if (i == 1 || i == 3 || i == 4) view_history(make_key(line[1], line[2], line[3]));
    841                 break;
    842             case 'G':
    843                 if (parse_uri(&line[1])) view_directory(parsed_host, parsed_port, parsed_selector, 1);
    844                 else puts("invalid gopher URI");
    845                 break;
    846             case 'B':
    847                 if (i == 1 || i == 3 || i == 4) view_bookmarks(make_key(line[1], line[2], line[3]));
    848                 break;
    849             default:
    850                 follow_link(make_key(line[0], line[1], line[2]));
    851                 break;
    852         }
    853     }
    854     return EXIT_SUCCESS; /* never get's here but stops cc complaining */
    855 }