/* * cgo - a simple terminal based gopher client * Copyright (c) 2019 Sebastian Steinhauer * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ #include #include #include #include #include #include #include #include #include #include #include #include /* some "configuration" */ #define START_URI "gopher://gopher.floodgap.com:70" #define CMD_TEXT "less" #define CMD_IMAGE "display" #define CMD_BROWSER "firefox" #define CMD_PLAYER "mplayer" #define CMD_TELNET "telnet" #define COLOR_PROMPT "1;34" #define COLOR_SELECTOR "1;32" #define HEAD_CHECK_LEN 5 #define GLOBAL_CONFIG_FILE "/etc/cgorc" #define LOCAL_CONFIG_FILE "/.cgorc" #define NUM_BOOKMARKS 20 #define VERBOSE "true" /* some internal defines */ #define KEY_RANGE (('z' - 'a') + 1) /* structs */ typedef struct link_s link_t; struct link_s { link_t *next; char which; short key; char *host; char *port; char *selector; }; typedef struct config_s config_t; struct config_s { char start_uri[512]; char cmd_text[512]; char cmd_image[512]; char cmd_browser[512]; char cmd_player[512]; char color_prompt[512]; char color_selector[512]; char verbose[512]; }; char tmpfilename[256]; link_t *links = NULL; link_t *history = NULL; int link_key; char current_host[512], current_port[64], current_selector[1024]; char parsed_host[512], parsed_port[64], parsed_selector[1024]; char bookmarks[NUM_BOOKMARKS][512]; config_t config; /* function prototypes */ int parse_uri(const char *uri); /* implementation */ void usage() { fputs("usage: cgo [-v] [-H] [gopher URI]\n", stderr); exit(EXIT_SUCCESS); } void banner(FILE *f) { fputs("cgo 0.6.1 Copyright (c) 2020 Sebastian Steinhauer\n", f); } int check_option_true(const char *option) { return strcasecmp(option, "false") && strcasecmp(option, "off"); } void parse_config_line(const char *line) { char token[1024]; char bkey[128]; char *value = NULL; int i, j; while (*line == ' ' || *line == '\t') line++; for (i = 0; *line && *line != ' ' && *line != '\t'; line++) if (i < sizeof(token) - 1) token[i++] = *line; token[i] = 0; if (! strcmp(token, "start_uri")) value = &config.start_uri[0]; else if (! strcmp(token, "cmd_text")) value = &config.cmd_text[0]; else if (! strcmp(token, "cmd_browser")) value = &config.cmd_browser[0]; else if (! strcmp(token, "cmd_image")) value = &config.cmd_image[0]; else if (! strcmp(token, "cmd_player")) value = &config.cmd_player[0]; else if (! strcmp(token, "color_prompt")) value = &config.color_prompt[0]; else if (! strcmp(token, "color_selector")) value = &config.color_selector[0]; else if (! strcmp(token, "verbose")) value = &config.verbose[0]; else { for (j = 0; j < NUM_BOOKMARKS; j++) { snprintf(bkey, sizeof(bkey), "bookmark%d", j+1); if (! strcmp(token, bkey)) { value = &bookmarks[j][0]; break; } } if (! value) return; }; while (*line == ' ' || *line == '\t') line++; for (i = 0; *line; line++) if (i < 512-1) value[i++] = *line; for (i--; i > 0 && (value[i] == ' ' || value[i] == '\t'); i--) ; value[++i] = 0; } void load_config(const char *filename) { FILE *fp; int ch, i; char line[1024]; fp = fopen(filename, "r"); if (! fp) return; memset(line, 0, sizeof(line)); i = 0; ch = fgetc(fp); while (1) { switch (ch) { case '#': while (ch != '\n' && ch != -1) ch = fgetc(fp); break; case -1: parse_config_line(line); fclose(fp); return; case '\r': ch = fgetc(fp); break; case '\n': parse_config_line(line); memset(line, 0, sizeof(line)); i = 0; ch = fgetc(fp); break; default: if (i < sizeof(line) - 1) line[i++] = ch; ch = fgetc(fp); break; } } } void init_config() { char filename[1024]; const char *home; int i; /* copy defaults */ snprintf(config.start_uri, sizeof(config.start_uri), START_URI); snprintf(config.cmd_text, sizeof(config.cmd_text), "%s", CMD_TEXT); snprintf(config.cmd_image, sizeof(config.cmd_image), "%s", CMD_IMAGE); snprintf(config.cmd_browser, sizeof(config.cmd_browser), "%s", CMD_BROWSER); snprintf(config.cmd_player, sizeof(config.cmd_player), "%s", CMD_PLAYER); snprintf(config.color_prompt, sizeof(config.color_prompt), "%s", COLOR_PROMPT); snprintf(config.color_selector, sizeof(config.color_selector), "%s", COLOR_SELECTOR); snprintf(config.verbose, sizeof(config.verbose), "%s", VERBOSE); for (i = 0; i < NUM_BOOKMARKS; i++) bookmarks[i][0] = 0; /* read configs */ load_config(GLOBAL_CONFIG_FILE); home = getenv("HOME"); if (home) { snprintf(filename, sizeof(filename), "%s%s", home, LOCAL_CONFIG_FILE); load_config(filename); } } int dial(const char *host, const char *port, const char *selector) { struct addrinfo hints; struct addrinfo *res, *r; int srv = -1, l; char request[512]; memset(&hints, 0, sizeof(hints)); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; if (getaddrinfo(host, port, &hints, &res) != 0) { fprintf(stderr, "error: cannot resolve hostname '%s:%s': %s\n", host, port, strerror(errno)); return -1; } for (r = res; r; r = r->ai_next) { srv = socket(r->ai_family, r->ai_socktype, r->ai_protocol); if (srv == -1) continue; if (connect(srv, r->ai_addr, r->ai_addrlen) == 0) break; close(srv); } freeaddrinfo(res); if (! r) { fprintf(stderr, "error: cannot connect to host '%s:%s'\n", host, port); return -1; } snprintf(request, sizeof(request), "%s\r\n", selector); l = strlen(request); if (write(srv, request, l) != l) { fprintf(stderr, "error: cannot complete request\n"); close(srv); return -1; } return srv; } int read_line(int fd, char *buf, size_t buf_len) { size_t i = 0; char c = 0; do { if (read(fd, &c, sizeof(char)) != sizeof(char)) return 0; if (c != '\r') buf[i++] = c; } while (c != '\n' && i < buf_len); buf[i - 1] = '\0'; return 1; } int download_file(const char *host, const char *port, const char *selector, int fd) { int srvfd, len; unsigned long total = 0; char buffer[4096]; if (check_option_true(config.verbose)) printf("downloading [%s]...\r", selector); srvfd = dial(host, port, selector); if (srvfd == -1) { printf("\033[2Kerror: downloading [%s] failed\n", selector); close(fd); return 0; } while ((len = read(srvfd, buffer, sizeof(buffer))) > 0) { write(fd, buffer, len); total += len; if (check_option_true(config.verbose)) printf("downloading [%s] (%ld kb)...\r", selector, total / 1024); } close(fd); close(srvfd); if (check_option_true(config.verbose)) printf("\033[2Kdownloading [%s] complete\n", selector); return 1; } int download_temp(const char *host, const char *port, const char *selector) { int tmpfd; #if defined(__OpenBSD__) strlcpy(tmpfilename, "/tmp/cgoXXXXXX", sizeof(tmpfilename)); #else strcpy(tmpfilename, "/tmp/cgoXXXXXX"); #endif tmpfd = mkstemp(tmpfilename); if (tmpfd == -1) { fputs("error: unable to create tmp file\n", stderr); return 0; } if (! download_file(host, port, selector, tmpfd)) { unlink(tmpfilename); return 0; } return 1; } int make_key(char c1, char c2, char c3) { if (! c1 || ! c2) return -1; if (! c3) return ((c1 - 'a') * KEY_RANGE) + (c2 - 'a'); else return (((c1 - 'a' + 1) * KEY_RANGE * KEY_RANGE) + ((c2 - 'a') * KEY_RANGE) + (c3 - 'a')); } void make_key_str(int key, char *c1, char *c2, char *c3) { if (key < (KEY_RANGE * KEY_RANGE)) { *c1 = 'a' + (key / KEY_RANGE); *c2 = 'a' + (key % KEY_RANGE); *c3 = 0; } else { *c1 = 'a' + (key / (KEY_RANGE * KEY_RANGE)) - 1; *c2 = 'a' + ((key / KEY_RANGE) % KEY_RANGE); *c3 = 'a' + (key % KEY_RANGE); } } void add_link(char which, const char *name, const char *host, const char *port, const char *selector) { link_t *link; char a = 0, b = 0, c = 0; if (! host || ! port || ! selector) return; /* ignore incomplete selectors */ link = calloc(1, sizeof(link_t)); link->which = which; link->key = link_key; link->host = strdup(host); link->port = strdup(port); link->selector = strdup(selector); if (! links) link->next = NULL; else link->next = links; links = link; make_key_str(link_key++, &a, &b, &c); printf("\033[%sm%c%c%c\033[0m \033[1m%s\033[0m\n", config.color_selector, a, b, c, name); } void clear_links() { link_t *link, *next; for (link = links; link; ) { next = link->next; free(link->host); free(link->port); free(link->selector); free(link); link = next; } links = NULL; link_key = 0; } void add_history() { link_t *link; link = calloc(1, sizeof(link_t)); link->host = strdup(current_host); link->port = strdup(current_port); link->selector = strdup(current_selector); link->which = 0; /* not needed for history...just clear them */ link->key = 0; if (! history) link->next = NULL; else link->next = history; history = link; } void handle_directory_line(char *line) { int i; char *lp, *last, *fields[4]; /* tokenize */ for (i = 0; i < 4; i++) fields[i] = NULL; last = &line[1]; for (lp = last, i = 0; i < 4; lp++) { if (*lp == '\t' || *lp == '\0') { fields[i] = last; last = lp + 1; if (*lp == '\0') break; *lp = '\0'; i++; } } /* determine listing type */ switch (line[0]) { case 'i': case '3': printf(" %s\n", fields[0]); break; case '.': /* some gopher servers use this */ puts(""); break; case '0': case '1': case '5': case '7': case '8': case '9': case 'g': case 'I': case 'p': case 'h': case 's': add_link(line[0], fields[0], fields[2], fields[3], fields[1]); break; default: printf("miss [%c]: %s\n", line[0], fields[0]); break; } } int is_valid_directory_entry(const char *line) { switch (line[0]) { case 'i': case '3': case '.': /* some gopher servers use this */ case '0': case '1': case '5': case '7': case '8': case '9': case 'g': case 'I': case 'p': case 'h': case 's': return 1; default: return 0; } } void view_directory(const char *host, const char *port, const char *selector, int make_current) { int is_dir; int srvfd, i, head_read; char line[1024]; char head[HEAD_CHECK_LEN][1024]; srvfd = dial(host, port, selector); if (srvfd != -1) { /* only adapt current prompt when successful */ /* make history entry */ if (make_current) add_history(); /* don't overwrite the current_* things... */ if (host != current_host) snprintf(current_host, sizeof(current_host), "%s", host); if (port != current_port) snprintf(current_port, sizeof(current_port), "%s", port); if (selector != current_selector) snprintf(current_selector, sizeof(current_selector), "%s", selector); } clear_links(); /* clear links *AFTER* dialing out!! */ if (srvfd == -1) return; /* quit if not successful */ head_read = 0; is_dir = 1; while (head_read < HEAD_CHECK_LEN && read_line(srvfd, line, sizeof(line))) { strcpy(head[head_read], line); if (!is_valid_directory_entry(head[head_read])) { is_dir = 0; break; } head_read++; } if (!is_dir) { puts("error: Not a directory."); close(srvfd); return; } for (i = 0; i < head_read; i++) { handle_directory_line(head[i]); } while (read_line(srvfd, line, sizeof(line))) { handle_directory_line(line); } close(srvfd); } void view_file(const char *cmd, const char *host, const char *port, const char *selector) { pid_t pid; int status, i, j; char buffer[1024], *argv[32], *p; if (check_option_true(config.verbose)) printf("h(%s) p(%s) s(%s)\n", host, port, selector); if (! download_temp(host, port, selector)) return; /* parsed command line string */ argv[0] = &buffer[0]; for (p = (char*) cmd, i = 0, j = 1; *p && i < sizeof(buffer) - 1 && j < 30; ) { if (*p == ' ' || *p == '\t') { buffer[i++] = 0; argv[j++] = &buffer[i]; while (*p == ' ' || *p == '\t') p++; } else buffer[i++] = *p++; } buffer[i] = 0; argv[j++] = tmpfilename; argv[j] = NULL; /* fork and execute */ if (check_option_true(config.verbose)) printf("executing: %s %s\n", cmd, tmpfilename); pid = fork(); if (pid == 0) { if (execvp(argv[0], argv) == -1) puts("error: execvp() failed!"); } else if (pid == -1) puts("error: fork() failed"); sleep(1); /* to wait for browsers etc. that return immediatly */ waitpid(pid, &status, 0); unlink(tmpfilename); } void view_telnet(const char *host, const char *port) { pid_t pid; int status; printf("executing: %s %s %s\n", CMD_TELNET, host, port); pid = fork(); if (pid == 0) { if (execlp(CMD_TELNET, CMD_TELNET, host, port, NULL) == -1) puts("error: execlp() failed!"); } else if (pid == -1) puts("error: fork() failed!"); waitpid(pid, &status, 0); puts("(done)"); } void view_download(const char *host, const char *port, const char *selector) { int fd; char filename[1024], line[1024]; snprintf(filename, sizeof(filename), "%s", strrchr(selector, '/') + 1); printf("enter filename for download [%s]: ", filename); fflush(stdout); if (! read_line(0, line, sizeof(line))) { puts("download aborted"); return; } if (strlen(line) > 0) #if defined(__OpenBSD__) strlcpy(filename, line, sizeof(filename)); #else strcpy(filename, line); #endif fd = open(filename, O_CREAT | O_WRONLY, S_IRUSR | S_IWUSR); if (fd == -1) { printf("error: unable to create file [%s]: %s\n", filename, strerror(errno)); return; } if (! download_file(host, port, selector, fd)) { printf("error: unable to download [%s]\n", selector); unlink(filename); return; } } void view_search(const char *host, const char *port, const char *selector) { char search_selector[1024]; char line[1024]; printf("enter search string: "); fflush(stdout); if (! read_line(0, line, sizeof(line))) { puts("search aborted"); return; } snprintf(search_selector, sizeof(search_selector), "%s\t%s", selector, line); view_directory(host, port, search_selector, 1); } void view_history(int key) { int history_key = 0; char a, b, c; link_t *link; if (! history) { puts("(empty history)"); return; } if ( key < 0 ) { puts("(history)"); for ( link = history; link; link = link->next ) { make_key_str(history_key++, &a, &b, &c); printf("\033[%sm%c%c%c\033[0m \033[1m%s:%s/1%s\033[0m\n", COLOR_SELECTOR, a, b, c, link->host, link->port, link->selector); } } else { /* traverse history list */ for ( link = history; link; link = link->next, ++history_key ) { if ( history_key == key ) { view_directory(link->host, link->port, link->selector, 0); return; } } puts("history item not found"); } } void view_bookmarks(int key) { int i; char a, b, c; if (key < 0) { puts("(bookmarks)"); for (i = 0; i < NUM_BOOKMARKS; i++) { if (bookmarks[i][0]) { make_key_str(i, &a, &b, &c); printf("\033[%sm%c%c%c\033[0m \033[1m%s\033[0m\n", COLOR_SELECTOR, a, b, c, &bookmarks[i][0]); } } } else { for (i = 0; i < NUM_BOOKMARKS; i++) { if (bookmarks[i][0] && i == key) { if (parse_uri(&bookmarks[i][0])) view_directory(parsed_host, parsed_port, parsed_selector, 0); else printf("invalid gopher URI: %s", &bookmarks[i][0]); return; } } } } void pop_history() { link_t *next; if (! history) { puts("(empty history)"); return; } /* reload page from history (and don't count as history) */ view_directory(history->host, history->port, history->selector, 0); /* history is history... :) */ next = history->next; free(history->host); free(history->port); free(history->selector); free(history); history = next; } int follow_link(int key) { link_t *link; for (link = links; link; link = link->next) { if (link->key != key) continue; switch (link->which) { case '0': view_file(&config.cmd_text[0], link->host, link->port, link->selector); break; case '1': view_directory(link->host, link->port, link->selector, 1); break; case '7': view_search(link->host, link->port, link->selector); break; case '5': case '9': view_download(link->host, link->port, link->selector); break; case '8': view_telnet(link->host, link->port); break; case 'g': case 'I': case 'p': view_file(&config.cmd_image[0], link->host, link->port, link->selector); break; case 'h': view_file(&config.cmd_browser[0], link->host, link->port, link->selector); break; case 's': view_file(&config.cmd_player[0], link->host, link->port, link->selector); break; default: printf("missing handler [%c]\n", link->which); break; } return 1; /* return the array is broken after view! */ } return 0; } void download_link(int key) { link_t *link; for (link = links; link; link = link->next) { if (link->key != key) continue; view_download(link->host, link->port, link->selector); return; } puts("link not found"); } int parse_uri(const char *uri) { int i; /* strip gopher:// */ if (! strncmp(uri, "gopher://", 9)) uri += 9; /* parse host */ for (i = 0; *uri && *uri != ':' && *uri != '/'; uri++) { if (*uri != ' ' && i < sizeof(parsed_host) - 1) parsed_host[i++] = *uri; } if (i > 0) parsed_host[i] = 0; else return 0; /* parse port */ if (*uri == ':') { uri++; for (i = 0; *uri && *uri != '/'; uri++) if (*uri != ' ' && i < sizeof(parsed_port) - 1) parsed_port[i++] = *uri; parsed_port[i] = 0; } else snprintf(parsed_port, sizeof(parsed_port), "%d", 70); /* parse selector (ignore slash and selector type) */ if (*uri) ++uri; if (*uri) ++uri; for (i = 0; *uri && i < sizeof(parsed_selector) - 1; ++uri, ++i) parsed_selector[i] = *uri; parsed_selector[i] = '\0'; return 1; } int main(int argc, char *argv[]) { int i; char line[1024], *uri; /* copy defaults */ init_config(); uri = &config.start_uri[0]; /* parse command line */ for (i = 1; i < argc; i++) { if (argv[i][0] == '-') switch(argv[i][1]) { case 'H': usage(); break; case 'v': banner(stdout); exit(EXIT_SUCCESS); default: usage(); } else { uri = argv[i]; } } /* parse uri */ if (! parse_uri(uri)) { banner(stderr); fprintf(stderr, "invalid gopher URI: %s", argv[i]); exit(EXIT_FAILURE); } /* main loop */ view_directory(parsed_host, parsed_port, parsed_selector, 0); for (;;) { printf("\033[%sm%s:%s%s\033[0m ", config.color_prompt, current_host, current_port, current_selector); fflush(stdout); /* to display the prompt */ if (! read_line(0, line, sizeof(line))) { puts("QUIT"); return EXIT_SUCCESS; } i = strlen(line); switch (line[0]) { case '?': puts( "? - help\n" "* - reload directory\n" "< - go back in history\n" ".[LINK] - download the given link\n" "H - show history\n" "H[LINK] - jump to the specified history item\n" "G[URI] - jump to the given gopher URI\n" "B - show bookmarks\n" "B[LINK] - jump to the specified bookmark item\n" "C^d - quit"); break; case '<': pop_history(); break; case '*': view_directory(current_host, current_port, current_selector, 0); break; case '.': download_link(make_key(line[1], line[2], line[3])); break; case 'H': if (i == 1 || i == 3 || i == 4) view_history(make_key(line[1], line[2], line[3])); break; case 'G': if (parse_uri(&line[1])) view_directory(parsed_host, parsed_port, parsed_selector, 1); else puts("invalid gopher URI"); break; case 'B': if (i == 1 || i == 3 || i == 4) view_bookmarks(make_key(line[1], line[2], line[3])); break; default: follow_link(make_key(line[0], line[1], line[2])); break; } } return EXIT_SUCCESS; /* never get's here but stops cc complaining */ }