You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

855 lines
24 KiB
C

/*
* cgo - a simple terminal based gopher client
* Copyright (c) 2019 Sebastian Steinhauer <s.steinhauer@yahoo.de>
*
* 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 <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <netdb.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
/* 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 */
}