/**************************************************************************** Simple gopher protocol client for the PANDA TOPS-20 operating system. Copyright (C) 2024 Robert Cunnings NW8L * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * This program is free software: you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see <http://www.gnu.org/licenses/>. * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Usage: gc [-c [cache_dir]] [-k] -g hostname [port] [selector] gc [-c [cache_dir]] [-k] -m [filename] gc -V The "-g" switch commands the program to connect to a gopher server. The host name can be followed by optional port number and gopher selector arguments. The default port number is 70. Alternatively, the "-m" switch tells the program to open a menu file stored on disk. The 'filename' argument is optional; the default is "gc-favs.gcm". The optional "-c" switch enables automatic caching of menu and text items downloaded from the server. This greatly speeds up moving back and forth through a series of previously visited links. The optional 'cache_dir' argument specifies a subdirectory holding cache files, e.g. "gc -c <.cache>" to keep cache files out of the way. The optional "-k" switch enables the cache "auto kill" feature. This is meant to be used together with the "-c" switch. It automatically kills the cache file for a page when backing up from it with the 'b' command at the "gc>" prompt. As you move forward through a series of pages with the 'f' and 'g' commands, cache files are created to speed up the reverse traversal of the series. Then, when moving back to the starting point with the 'b' command, the cache files are deleted along the way to conserve disk space. The "-V" switch prints the gc version number and exits. After a gopher menu or text item is fetched and displayed, the following keyboard commands are available at the "gc>" prompt. This appears after paging to the end of the text or menu. You can also press CTRL/O before reaching the end to stop paging, followed by ENTER to show the prompt. 'q' - (q)uit the program. 'd' - re(d)raw the current page starting over from the top. 'r' - (r)eload the current page. Downloads the page from the server, overwriting the copy in the cache, then redraws it. 'b' - move (b)ack to the previous page. The cached copy will be loaded. 'h' - open the current host's (h)ome menu. 'i' - Show (i)nfo for current page. Host, port, selector, item type, number of bytes, and name of the cache file are shown. 'iN' - Show (i)nfo for menu item where N is the link number of the item, in range 1 to 999. Host, port, selector, item type and name of the cache file (if one exists) are shown. 'fN' - move (f)orward where N is a link number in range 1 to 999. Works only for links to displayable item types '0' (text) and '1' (menu). 's' - (s)ave to disk the content of the current page. Enter a file name at the prompt. An empty name cancels the operation. 'sN' - (s)ave to disk where N is the link number of the item to save, in range 1 to 999. Enter a file name at the prompt. An empty name cancels the operation. 'c' - (c)ache the current page, useful if gc was started with caching disabled. Overwrites any existing cache file for the page. 'k' - (k)ill cached copy of current page. When caching is enabled, use this to save disk space if you don't plan to revisit the page. 'g host [port] [selector]' - (g)o to gopher server "host". The host name may be followed by an optional port number and optional selector. The default port number is 70. 'a [filename]' - (a)dd the current page to the favorites menu. An optional file name argument can be used to specify a favorites file to store it to. Enter a title for the favorites menu link at the prompt. 'v [filename]' - (v)iew the favorites menu. An optional file name argument can be used to specify the favorites file to view. '?' - show list of available commands. A history of pages visited is kept to enable the "back" command. The size of the history buffer is defined by the HISTORY_SIZE macro. It is a circular buffer - when it's full, backing up past the oldest page takes you to the newest page. When item types '0' (text) and '1' (menu) are downloaded, they are cached on disk in files with extension ".gcc". Subsequent views of the page are served from the cache to save time and bandwidth. When viewing a page, you can use the 'r' (reload) command to force a download from the server to over- write the cached copy. You can use the 'k' (kill) command to delete the page from the cache to save disk space if you won't be visiting it again. All cache files can be deleted with "del *.gcc" to free up disk space. Note: this program attempts to be compatible with KCC-6 on Panda TOPS-20. To compile, use 'cc -i=extend gc <kcc-6.lib.user>trmcap' to build an executable that runs with extended addressing. This increases the amount of dynamic memory available for buffering downloaded text. Ignore the "Missing function prototype" advisories generated by the termcap library during compilation. ****************************************************************************/ #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <sys/socket.h> #include <netinet/in.h> #include <netinet/tcp.h> #include <netdb.h> #include <trmcap.h> #include <ctype.h> #include <sys/stat.h> #include <errno.h> #define PAGE_BLOCK_SIZE 2048 #define MENU_BLOCK_SIZE 512 #define MAX_MENU_LINK 999 #define MAX_CACHE_DIR_SIZE 32 #define HISTORY_SIZE 10 #define VERSION "1.00" #define DEF_FAVS_FNAME "gc-favs.gcm" typedef struct hist_t { short port_num; short item_type; short is_query; short is_fav_menu; short is_ip_addr; short save_to_disk; short force_request; short use_selector; char selector[128]; char query[64]; char host[64]; } HISTORY; typedef struct mline_t { short item_type; short link_num; char *desc; char *selector; char *host; char *port_num; } MENU_LINE; typedef struct page_t { int num_bytes; int buf_size; int num_lines; int max_linknum; int menu_size; char *buf; char *temp; MENU_LINE *menu; } PAGE; /* the one and only page buffer with text of current page */ PAGE page; /* termcap variables */ char PC, *BC, *UP, *CL, *CE, *CM, *tg; short ospeed, CO, LI; char cfile_dir[MAX_CACHE_DIR_SIZE+1]; char input_str[72]; char cap_buf[512], *table; char term_buf[2048]; /* data buffers */ char req_buffer[256]; char resp_buffer[4096]; /* browsing history */ HISTORY hist[HISTORY_SIZE]; short cur_page; short prev_page; short max_page; int fletch16(char *data, int cnt) { /* compute fletcher16 checksum. Using 9-bit chars so it's mod (512-1) */ int i, sum1, sum2; sum1 = 0; sum2 = 0; for (i = 0; i < cnt; i++) { sum1 = (sum1 + data[i]) % 511; sum2 = (sum2 + sum1) % 511; } return (sum2 << 9) | sum1; } int test_cache(char *host, char *selector, char *query, int is_query) { int check; struct stat st; if (is_query) sprintf(req_buffer, "%s:%s\t%s", host, selector, query); else sprintf(req_buffer, "%s:%s", host, selector); check = fletch16(req_buffer, strlen(req_buffer)); sprintf(req_buffer, "%s%06d.gcc", cfile_dir, check); if (stat(req_buffer, &st) != 0) return -1; /* no cache file found */ return check; } void pr_text(void) { int i; for (i = 0; i < page.num_bytes; i++) putchar(page.buf[i]); putchar('\n'); } int is_item_type(int c) { switch (c) { case 'i': case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case 'T': case '9': case 'g': case 'I': case 'h': case 'p': case 'P': case 'd': case 's': case 'X': case 'r': case ':': case ';': case '<': break; default: return 0; break; } return 1; } int mk_menu(void) { int i, c, cnt, linknum, state; MENU_LINE *temp_ptr; page.menu_size = MENU_BLOCK_SIZE; page.menu = (MENU_LINE *)malloc(page.menu_size * sizeof(MENU_LINE)); if (page.menu == NULL) { printf("Error: out of memory! Exiting.\n"); exit(1); } cnt = 0; state = 0; linknum = 1; for (i = 0; i < page.num_bytes; i++) { c = page.buf[i]; if (c == '\0') /* end of buffer */ break; if (state == -1) /* "period on a line by itself" detected, stop */ break; switch(state) { case 0: if (c == '.') { if (page.buf[i + 1] == '\r' || page.buf[i + 1] == '\n') { /* "period on a line by itself" indicates EOT */ page.buf[i] = '\0'; state = -1; continue; } } if (c == '\r' || c == '\n') { /* unexpected EOL */ page.buf[i] = '\0'; continue; } if (is_item_type(c)) page.menu[cnt].item_type = c; else continue; page.menu[cnt].desc = &page.buf[i + 1]; ++state; break; case 1: if (c == '\r' || c == '\n') { /* unexpected EOL */ page.buf[i] = '\0'; state = 4; continue; } if (c == '\t') { page.buf[i] = '\0'; page.menu[cnt].selector = &page.buf[i + 1]; ++state; } break; case 2: if (c == '\r' || c == '\n') { /* unexpected EOL */ page.buf[i] = '\0'; state = 4; continue; } if (c == '\t') { page.buf[i] = '\0'; page.menu[cnt].host = &page.buf[i + 1]; ++state; } break; case 3: if (c == '\r' || c == '\n') { /* unexpected EOL */ page.buf[i] = '\0'; state = 4; continue; } if (c == '\t') { page.buf[i] = '\0'; page.menu[cnt].port_num = &page.buf[i + 1]; ++state; } break; case 4: if (c == '\t') { /* discard any gopher+ field following port number */ page.buf[i] = '\0'; continue; } if (c == '\r' || c == '\n') { page.buf[i] = '\0'; /* skip the next CR or LF */ if (page.buf[i + 1] == '\r' || page.buf[i + 1] == '\n') ++i; switch (page.menu[cnt].item_type) { case 'i': case '2': case '3': case '8': case 'T': case '+': case 'h': break; default: page.menu[cnt].link_num = linknum++; break; } ++cnt; state = 0; } break; } if (linknum == MAX_MENU_LINK) break; if (cnt == page.menu_size) { /* need to allocate more memory. Normally realloc() would be used in this situation but the version in the KCC standard library doesn't work reliably - it often fails with message "Error: old block not in use (flag == FREE)". However, the workaround using malloc() and free() always works. */ page.menu_size += MENU_BLOCK_SIZE; temp_ptr = (MENU_LINE *)malloc(page.menu_size * sizeof(MENU_LINE)); if (temp_ptr == NULL) { printf("Error: out of memory! Exiting.\n"); exit(1); } memcpy(temp_ptr, page.menu, cnt * sizeof(MENU_LINE)); sleep(1); /* seems necessary to prevent error on free() */ free((MENU_LINE *)page.menu); page.menu = temp_ptr; } } page.max_linknum = linknum - 1; return cnt; } void pr_menu(void) { int i; char *it; for (i = 0; i < page.num_lines; i++) { switch (page.menu[i].item_type) { case '0': it = "TXT"; break; case '1': it = "DIR"; break; case '2': it = "CSO"; break; case '3': it = "ERR"; break; case '4': it = "MAC"; break; case '5': it = "DOS"; break; case '6': it = "UUE"; break; case '7': it = "QRY"; break; case '8': case 'T': it = "TEL"; break; case '9': it = "BIN"; break; case 'g': it = "GIF"; break; case 'I': it = "IMG"; break; case 'h': it = "HTM"; break; case 'p': it = "PNG"; break; case 'P': it = "PDF"; break; case 'd': it = "DOC"; break; case 's': it = "WAV"; break; case 'X': it = "XML"; break; case 'r': it = "RTF"; break; case ':': it = "BMP"; break; case ';': it = "MOV"; break; case '<': it = "SND"; break; default: it = "---"; break; } switch (page.menu[i].item_type) { case 'i': printf("%s\n", page.menu[i].desc); break; case '2': case '3': case '8': case 'T': case '+': case 'h': printf(" (%s) %s\n", it, page.menu[i].desc); break; default: printf("[%03d] (%s) %s\n", page.menu[i].link_num, it, page.menu[i].desc); break; } } } void clr_screen(void) { tputs(CL, 1, putchar); } void clr_to_eol(void) { tputs(CE, 1, putchar); } void goto_xy(int x, int y) { char *tg; tg = tgoto(CM, x, y, putchar); tputs(tg, 1, putchar); } int init_term(void) { int result; char *temp_str; temp_str = getenv("TERM"); if (temp_str == NULL) { printf("Error: failed to get terminal type!\n"); return -1; } result = tgetent(term_buf, temp_str); if (result < 0) { printf("Error: failed to get caps for %s!\n", temp_str); return -1; } table = cap_buf; BC = tgetstr("bc", &table); CL = tgetstr("cl", &table); CE = tgetstr("ce", &table); CM = tgetstr("cm", &table); UP = tgetstr("up", &table); CO = tgetnum("co"); LI = tgetnum("li"); temp_str = tgetstr("pc", &table); PC = temp_str ? temp_str[0] : '\0'; return 0; } int get_input_str(char buf[], int maxlen) { int c, i; i = 0; c = getchar(); while (c != '\n' && c != EOF) { if (isprint(c) && i < maxlen) buf[i++] = c; c = getchar(); } buf[i] = '\0'; return i; } int guess_item_type(char *selector) { int i, j, type; char *p, temp[64]; type = 0; for (i = strlen(selector) - 1; i >= 0; i--) { if (selector[i] == '.') { p = &selector[i]; j = 0; while (*p) temp[j++] = tolower(*p++); temp[j] = '\0'; if (!strcmp(temp, ".txt")) type = '0'; else if (!strcmp(temp, ".gif")) type = 'g'; else if (!strcmp(temp, ".htm")) type = 'h'; else if (!strcmp(temp, ".html")) type = 'h'; else if (!strcmp(temp, ".pdf")) type = 'P'; else if (!strcmp(temp, ".xml")) type = 'X'; else if (!strcmp(temp, ".rtf")) type = 'r'; else if (!strcmp(temp, ".png")) type = 'p'; else if (!strcmp(temp, ".doc")) type = 'd'; else if (!strcmp(temp, ".hex")) type = '4'; else if (!strcmp(temp, ".jpg")) type = 'I'; else if (!strcmp(temp, ".jpeg")) type = 'I'; else if (!strcmp(temp, ".tif")) type = 'I'; else if (!strcmp(temp, ".bmp")) type = ':'; else if (!strcmp(temp, ".wav")) type = 's'; else if (!strcmp(temp, ".mp3")) type = '<'; else if (!strcmp(temp, ".ogg")) type = '<'; else if (!strcmp(temp, ".gz")) type = '9'; else if (!strcmp(temp, ".tgz")) type = '9'; else if (!strcmp(temp, ".tar")) type = '9'; else if (!strcmp(temp, ".zip")) type = '9'; break; } if (selector[i] == '/') { type = '1'; break; } } return type; } int cache_page(int which) { int i, check; char fname_str[16]; FILE *file; if (hist[which].is_query) sprintf(req_buffer, "%s:%s\t%s", hist[which].host, hist[which].selector, hist[which].query); else sprintf(req_buffer, "%s:%s", hist[which].host, hist[which].selector); check = fletch16(req_buffer, strlen(req_buffer)); sprintf(fname_str, "%s%06d.gcc", cfile_dir, check); file = fopen(fname_str, "w"); if (file == NULL) return -1; for (i = 0; i < page.num_bytes; i++) { if (isprint(page.buf[i]) || isspace(page.buf[i])) fputc(page.buf[i], file); } fputc('\0', file); fclose(file); return check; } int load_page(int check) { int c, cnt, numbytes; char fname_str[16]; FILE *file; struct stat st; sprintf(fname_str, "%s%06d.gcc", cfile_dir, check); if (stat(fname_str, &st) != 0) return 0; numbytes = st.st_size; file = fopen(fname_str, "r"); if (file == NULL) return 0; cnt = 0; while ((c = fgetc(file)) && cnt < numbytes) { page.buf[cnt++] = c; if (cnt >= page.buf_size - 1) { /* need to allocate more memory. Normally realloc() would be used in this situation but the version in the KCC standard library doesn't work reliably - it often fails with message "Error: old block not in use (flag == FREE)". However, the workaround using malloc() and free() always works. */ page.buf_size += PAGE_BLOCK_SIZE; page.temp = (char *)malloc(page.buf_size); if (page.temp == NULL) { printf("Error: out of memory! Exiting.\n"); sleep(1); /* seems necessary to prevent error on free() */ free((char *)page.buf); exit(1); } page.buf[cnt] = '\0'; strcpy(page.temp, page.buf); sleep(1); /* seems necessary to prevent error on free() */ free((char *)page.buf); page.buf = page.temp; page.temp = NULL; } } page.buf[cnt] = '\0'; page.num_bytes = cnt; fclose(file); return 1; } int load_favorites(void) { int c, cnt, numbytes; FILE *file; struct stat st; if (stat(hist[cur_page].selector, &st) != 0) return 0; numbytes = st.st_size; file = fopen(hist[cur_page].selector, "r"); if (file == NULL) return 0; cnt = 0; while ((c = fgetc(file)) && cnt < numbytes) { page.buf[cnt++] = c; if (cnt >= page.buf_size - 1) { /* need to allocate more memory. Normally realloc() would be used in this situation but the version in the KCC standard library doesn't work reliably - it often fails with message "Error: old block not in use (flag == FREE)". However, the workaround using malloc() and free() always works. */ page.buf_size += PAGE_BLOCK_SIZE; page.temp = (char *)malloc(page.buf_size); if (page.temp == NULL) { printf("Error: out of memory! Exiting.\n"); sleep(1); /* seems necessary to prevent error on free() */ free((char *)page.buf); exit(1); } page.buf[cnt] = '\0'; strcpy(page.temp, page.buf); sleep(1); /* seems necessary to prevent error on free() */ free((char *)page.buf); page.buf = page.temp; page.temp = NULL; } } page.num_bytes = cnt; fclose(file); return 1; } int hist_init(void) { prev_page = 0; cur_page = 1; max_page = cur_page; memcpy(&hist[prev_page], &hist[cur_page], sizeof(HISTORY)); return cur_page; } int hist_next(void) { prev_page = cur_page; ++cur_page; if (max_page < HISTORY_SIZE) ++max_page; if (cur_page == HISTORY_SIZE) cur_page = 0; /* copy previous contents, important for commands like "home" and "save" which do not overwrite all fields */ memcpy(&hist[cur_page], &hist[prev_page], sizeof(HISTORY)); /* clear special flags */ hist[cur_page].is_query = 0; hist[cur_page].is_fav_menu = 0; hist[cur_page].save_to_disk = 0; hist[cur_page].force_request = 0; return cur_page; } int hist_prev(void) { if (max_page < HISTORY_SIZE && prev_page == 0) return cur_page; /* at start of history; can't go back */ --cur_page; if (cur_page < 0) cur_page = HISTORY_SIZE - 1; prev_page = cur_page - 1; if (prev_page < 0) prev_page = HISTORY_SIZE - 1; return cur_page; } void reset_page(void) { if (page.menu != NULL) { free((MENU_LINE *)page.menu); page.menu = NULL; } free((char *)page.buf); /* initialize the page structure */ memset(&page, 0, sizeof(PAGE)); page.buf_size = PAGE_BLOCK_SIZE; page.buf = (char *)malloc(page.buf_size); if (page.buf == NULL) { printf("Error: out of memory! Exiting.\n"); exit(1); } memset(page.buf, 0, page.buf_size); } FILE* open_file(char *fname, int type) { int temp; struct stat st; FILE *file; temp = stat(fname, &st); if (temp == 0) { printf("\nFile %s exists. Overwrite it? [y/N]\n", fname); get_input_str(input_str, 2); if (input_str[0] != 'y' && input_str[0] != 'Y') return NULL; } switch (type) { case '0': case '1': case '4': case '6': case 'h': file = fopen(fname, "w"); break; default: file = fopen(fname, "wb8"); break; } return file; } int write_file(FILE *file, char *buf, int numbytes, int type) { int i, cnt; cnt = 0; switch (type) { case '0': case '1': case '4': case '6': case 'h': cnt = 0; for (i = 0; i < numbytes; i++) { if (isprint(buf[i]) || isspace(buf[i])) { fputc(buf[i], file); ++cnt; } } break; default: cnt = fwrite(buf, 1, numbytes, file); break; } return cnt; } int add_to_favorites(char *fname) { char *p; FILE *file; /* append line for current page to favorites menu */ printf("\nAdd to favorites. Enter title: "); if (get_input_str(input_str, 40) == 0) { printf("Add to favorites canceled!\n"); fflush(stdout); sleep(2); return 0; } file = fopen(fname, "a"); if (file == NULL) return 0; /* although not sanctioned by RFC 1436, some gopher holes have dotted-quad IP addresses in menus vice host names */ sprintf(req_buffer, "%c%s\t%s\t%s\t%d\r\n", hist[cur_page].item_type, input_str, hist[cur_page].selector, hist[cur_page].host, hist[cur_page].port_num); p = req_buffer; while (*p) fputc(*p++, file); fclose(file); } int is_saveable(int item_type) { switch (item_type) { case '2': case '3': case '7': case '8': case 'T': case '+': break; default: return 1; } return 0; } int is_displayable(int item_type) { switch (item_type) { case '0': case '1': case '7': break; default: return 0; } return 1; } int fav_menu_init(char *fname) { struct stat st; if (stat(fname, &st) != 0) return 0; strcpy(hist[cur_page].selector, fname); strcpy(hist[cur_page].host, "localhost"); hist[cur_page].port_num = 70; hist[cur_page].item_type = '1'; hist[cur_page].is_ip_addr = 0; hist[cur_page].is_fav_menu = 1; /* initialize page buffer */ page.buf_size = PAGE_BLOCK_SIZE; page.buf = (char *)malloc(page.buf_size); if (page.buf == NULL) { printf("Error: out of memory! Exiting.\n"); exit(1); } /* copy settings to prev page buffer */ memcpy(&hist[prev_page], &hist[cur_page], sizeof(HISTORY)); return 1; } int is_ip_addr(char *host) { char *temp; temp = host; while (*temp) { if (!isdigit(*temp) && *temp != '.') return 0; ++temp; } return 1; } void goph_srv_init(int argc, char **argv, int index) { char *temp_str; int i; /* Capture the host name or IP address */ hist[cur_page].is_ip_addr = is_ip_addr(argv[index]); strcpy(hist[cur_page].host, argv[index]); ++index; /* is a port number specified? If so, capture it */ if (argc > index) { temp_str = argv[index]; if (isdigit(temp_str[0])) { i = atoi(temp_str); if (i > 0) hist[cur_page].port_num = i; ++index; } } /* many sites reject empty selector even though allowed by RFC1436 */ strcpy(hist[cur_page].selector, "/"); hist[cur_page].use_selector = 1; /* is a selector specified? If so, capture it */ if (argc > index) { temp_str = argv[index]; strcpy(hist[cur_page].selector, temp_str); if (temp_str[0] == '"') { for (i = 1; i < 64 && temp_str[i]; i++) { if (temp_str[i] != '"') { hist[cur_page].selector[i - 1] = temp_str[i]; } else { hist[cur_page].selector[i - 1] = '\0'; break; } } } else { strcpy(hist[cur_page].selector, temp_str); } hist[cur_page].use_selector = 1; } /* initialize the page structure */ memset(&page, 0, sizeof(PAGE)); /* initialize the history record */ if (hist[cur_page].use_selector) hist[cur_page].item_type = guess_item_type(hist[cur_page].selector); else hist[cur_page].item_type = '1'; /* initialize page buffer */ page.buf_size = PAGE_BLOCK_SIZE; page.buf = (char *)malloc(page.buf_size); if (page.buf == NULL) { printf("Error: out of memory! Exiting.\n"); exit(1); } /* copy settings to prev page buffer */ memcpy(&hist[prev_page], &hist[cur_page], sizeof(HISTORY)); } void print_usage(void) { printf("Usage: gc [-c [cache_dir]] -g hostname [port] [selector]\n"); printf(" gc [-c [cache_dir]] -m [filename]\n"); printf(" gc -V\n"); } int main(int argc, char **argv) { char *temp_str1, *temp_str2; unsigned long addr; int i, temp, sock, status, cnt; int is_redraw, cache_enabled, auto_kill; struct sockaddr_in sa; struct hostent *hent; FILE *file; if (argc == 1) { print_usage(); exit(1); } /* initialize terminal control */ if (init_term() < 0) exit(1); /* initialize browsing history */ hist_init(); hist[cur_page].port_num = 70; cache_enabled = 0; auto_kill = 0; /* process command line arguments */ for (i = 1; i < argc; i++) { if (!strcmp(argv[i], "-V")) { printf("gc %s\n", VERSION); exit(0); } else if (!strcmp(argv[i], "-c")) { cache_enabled = 1; /* look for cache directory arg - default is cwd */ if (argc > i + 1) { temp_str1 = argv[i + 1]; if (temp_str1[0] != '-') { if (strlen(temp_str1) > MAX_CACHE_DIR_SIZE) temp_str1[MAX_CACHE_DIR_SIZE] = '\0'; strcpy(cfile_dir, temp_str1); ++i; } } } else if (!strcmp(argv[i], "-k")) { /* enable automatic killing of cache file when moving "back" */ auto_kill = 1; } else if (!strcmp(argv[i], "-m")) { /* load menu file - default is 'favorites' menu */ if (argc > i + 1) temp_str1 = argv[i + 1]; else temp_str1 = DEF_FAVS_FNAME; if (fav_menu_init(temp_str1)) { goto favorites; } else { printf("Error: cannot open favorites file!\n"); exit(1); } break; } else if (!strcmp(argv[i], "-g")) { /* "go" to gopher server */ goph_srv_init(argc, argv, i + 1); break; } else { print_usage(); exit(1); } } restart: /* is this a save to disk operation? If so, get file name */ if (hist[cur_page].save_to_disk) { printf("\nSave to disk. Enter file name: "); temp = get_input_str(input_str, 30); if (temp) file = open_file(input_str, hist[cur_page].item_type); if (temp == 0) { printf("Save to disk canceled!\n"); fflush(stdout); sleep(2); } if (temp == 0 || file == NULL) { /* go back to previous page */ hist_prev(); if (hist[cur_page].is_fav_menu) goto favorites; else goto restart; } /* is data already in page buffer? If so, store it now */ if (hist[cur_page].save_to_disk == 2) { for (i = 0; i < page.num_bytes; i++) fputc(page.buf[i], file); fputc('\0', file); fclose(file); hist[cur_page].save_to_disk = 0; goto redraw; } } /* is this a query? If so, get query string */ else if (hist[cur_page].is_query == 1) { printf("\nEnter query: "); temp = get_input_str(input_str, 60); if (temp == 0) { /* go back to previous page */ hist_prev(); if (hist[cur_page].is_fav_menu) goto favorites; else goto restart; } strcpy(hist[cur_page].query, input_str); hist[cur_page].item_type = '1'; /* expect a gopher menu */ /* increment to prevent repeat of prompt when moving 'back' etc. */ ++hist[cur_page].is_query; } /* is this page cached? If so, reload from cache if request not forced */ if (!hist[cur_page].force_request) { temp = test_cache(hist[cur_page].host, hist[cur_page].selector, hist[cur_page].query, hist[cur_page].is_query); if (temp != -1) { is_redraw = 0; if (load_page(temp)) goto redraw; } } /* resolve ip address and create client socket */ if (hist[cur_page].is_ip_addr) { addr = inet_addr(hist[cur_page].host); } else { hent = gethostbyname(hist[cur_page].host); } memset(&sa, 0, sizeof(sa)); sa.sin_family = AF_INET; sa.sin_port = htons(hist[cur_page].port_num); if (hist[cur_page].is_ip_addr) sa.sin_addr.s_addr = (unsigned int)addr; else sa.sin_addr.s_addr = *((unsigned long *)hent->h_addr); sock = socket(AF_INET, SOCK_STREAM, 0); if (sock < 0) { printf("Error: failed to allocate socket! Exiting.\n"); exit(1); } /* attempt to connect to the server */ status = connect(sock, (struct sockaddr *)&sa, sizeof(sa)); if (status < 0) { printf("Error: failed to connect to: %s!\n", hist[cur_page].host); fflush(stdout); sleep(3); /* go back to previous page */ hist_prev(); reset_page(); if (hist[cur_page].is_fav_menu) goto favorites; else goto restart; } /* clear the screen and transmit the selector */ clr_screen(); goto_xy(0, 0); clr_to_eol(); if (hist[cur_page].use_selector && hist[cur_page].is_query) sprintf(req_buffer, "%s\t%s\r\n", hist[cur_page].selector, hist[cur_page].query); else if (hist[cur_page].use_selector) sprintf(req_buffer, "%s\r\n", hist[cur_page].selector); else sprintf(req_buffer, "\r\n"); cnt = write(sock, req_buffer, strlen(req_buffer)); if (cnt < 0) { printf("Error: could not send selector! Exiting.\n"); close(sock); exit(0); } if (hist[cur_page].save_to_disk) { printf("\n%s:%d %s '%c'\n", hist[cur_page].host, hist[cur_page].port_num, hist[cur_page].selector, hist[cur_page].item_type); } /* receive response from server */ printf("\nReceiving: "); fflush(stdout); while (1) { cnt = read(sock, resp_buffer, sizeof(resp_buffer)); putchar('#'); fflush(stdout); if (cnt <= 0) break; if (hist[cur_page].save_to_disk) { if (write_file(file, resp_buffer, cnt, hist[cur_page].item_type) < 0) { printf("Error: save to disk failed!\n"); close(sock); exit(1); } } else { resp_buffer[cnt] = '\0'; if (page.num_bytes + cnt >= page.buf_size) { /* need to allocate more memory. Normally realloc() would be used in this situation but the version in the KCC standard library doesn't work reliably - it often fails with message "Error: old block not in use (flag == FREE)". However, the workaround using malloc() and free() always works. */ page.buf_size += PAGE_BLOCK_SIZE; page.temp = (char *)malloc(page.buf_size); if (page.temp == NULL) { printf("Error: out of memory! Exiting.\n"); sleep(1); /* seems necessary to prevent error on free() */ free((char *)page.buf); close(sock); exit(1); } strcpy(page.temp, page.buf); sleep(1); /* seems necessary to prevent error on free() */ free((char *)page.buf); page.buf = page.temp; page.temp = NULL; } page.num_bytes += cnt; strcat(page.buf, resp_buffer); } sleep(1); } close(sock); sock = NULL; /* is this a save to disk operation? If so, close file and restart */ if (hist[cur_page].save_to_disk) { fclose(file); file = NULL; /* saved to disk, go back to previous page */ hist_prev(); reset_page(); if (hist[cur_page].is_fav_menu) goto favorites; else goto restart; } /* if this is a menu or text item type, cache it a in disk file */ else if (cache_enabled && (hist[cur_page].item_type == '0' || hist[cur_page].item_type == '1')) { temp = test_cache(hist[cur_page].host, hist[cur_page].selector, hist[cur_page].query, hist[cur_page].is_query); if (temp == -1) { if (cache_page(cur_page) != -1) hist[cur_page].force_request = 0; } } favorites: /* is this the favorites menu? If so, load data from favorites file */ if (hist[cur_page].is_fav_menu) { if (!load_favorites()) { /* file not found, go back to previous page */ hist_prev(); reset_page(); if (hist[cur_page].is_fav_menu) goto favorites; else goto restart; } } /* display the response, whether a gopher menu or a text file */ is_redraw = 0; redraw: clr_screen(); goto_xy(0, 0); clr_to_eol(); if (hist[cur_page].is_query) printf("\n%s:%d %s?%s '%c' (%d bytes)", hist[cur_page].host, hist[cur_page].port_num, hist[cur_page].selector, hist[cur_page].query, hist[cur_page].item_type, page.num_bytes); else printf("\n%s:%d %s '%c' (%d bytes)", hist[cur_page].host, hist[cur_page].port_num, hist[cur_page].selector, hist[cur_page].item_type, page.num_bytes); printf("\n\n"); switch (hist[cur_page].item_type) { case '1': if (!is_redraw) page.num_lines = mk_menu(); pr_menu(); break; default: pr_text(); break; } get_input: printf("\ngc> "); get_input_str(input_str, 30); if (input_str[0] == 'q') { /* "quit" command, exit the program */ goto end; } else if (input_str[0] == 'd') { /* "(re)draw" page, starting over from the top */ is_redraw = 1; goto redraw; } else if (input_str[0] == 'r') { /* is this a favorites menu? If so, redraw it */ if (hist[cur_page].is_fav_menu) { is_redraw = 1; goto redraw; } /* "refresh" page by downloading it from server again */ hist[cur_page].force_request = 1; reset_page(); goto restart; } else if (input_str[0] == 'b') { /* is "auto kill" of cache files enabled? */ if (auto_kill && !hist[cur_page].is_fav_menu) { temp = test_cache(hist[cur_page].host, hist[cur_page].selector, hist[cur_page].query, hist[cur_page].is_query); if (temp != -1) { sprintf(req_buffer, "%s%06d.gcc", cfile_dir, temp); unlink(req_buffer); } } /* "back" command, move to previous page */ temp = cur_page; hist_prev(); /* cur_page unchanged if at start when history buffer not yet full */ if (temp == cur_page) { printf("At start, cannot go back!\n"); goto get_input; } if (hist[cur_page].is_fav_menu) goto favorites; /* take shortcut for favorites menu */ reset_page(); goto restart; } else if (input_str[0] == 'h') { /* "home" command, move to root menu */ if (hist[cur_page].is_fav_menu) { printf("Cannot go 'home' from favorites menu!\n"); goto get_input; } hist_next(); reset_page(); hist[cur_page].item_type = '1'; hist[cur_page].selector[0] = '\0'; hist[cur_page].use_selector = 0; goto restart; } else if (input_str[0] == 's' && input_str[1] == '\0') { /* "save" command to store the currently displayed page */ if (hist[cur_page].is_fav_menu) { printf("Favorites menu already saved as file!\n"); goto get_input; } if (hist[cur_page].item_type == '0') { /* save text directly from current page buffer */ hist[cur_page].save_to_disk = 2; goto restart; } /* can't save from buffer, download from server */ hist_next(); hist[cur_page].save_to_disk = 1; if (hist[cur_page].selector[0] == '\0') hist[cur_page].use_selector = 0; else hist[cur_page].use_selector = 1; reset_page(); goto restart; } else if (input_str[0] == 's' && input_str[1] != '\0') { /* "save" command with link number argument */ temp_str1 = &input_str[1]; while (*temp_str1 && *temp_str1 == ' ') ++temp_str1; if (temp > 0 && temp <= page.max_linknum) { for (i = 0; i < page.num_lines; i++) { if (page.menu[i].link_num == temp) break; } if (!is_saveable(page.menu[i].item_type)) { printf("Error: item type '%c' cannot be saved to disk!\n", page.menu[i].item_type); goto get_input; } hist_next(); /* although not sanctioned by RFC 1436, some gopher holes have dotted-quad IP addresses in menus vice host names */ hist[cur_page].is_ip_addr = is_ip_addr(page.menu[i].host); strcpy(hist[cur_page].host, page.menu[i].host); strcpy(hist[cur_page].selector, page.menu[i].selector); hist[cur_page].port_num = atoi(page.menu[i].port_num); hist[cur_page].item_type = page.menu[i].item_type; if (hist[cur_page].selector[0] == '\0') hist[cur_page].use_selector = 0; else hist[cur_page].use_selector = 1; reset_page(); goto restart; } else { printf("Error: link number %d is out of range!\n", i); goto get_input; } } else if (input_str[0] == 'f' && hist[cur_page].item_type == '1') { /* "foward" command with a link number argument */ temp_str1 = &input_str[1]; while (*temp_str1 && *temp_str1 == ' ') ++temp_str1; /* skip spaces */ temp = atoi(temp_str1); if (temp > 0 && temp <= page.max_linknum) { for (i = 0; i < page.num_lines; i++) { if (page.menu[i].link_num == temp) break; } if (!is_displayable(page.menu[i].item_type)) { printf("Error: item type '%c' cannot be displayed!\n", page.menu[i].item_type); goto get_input; } hist_next(); /* although not sanctioned by RFC 1436, some gopher holes have dotted-quad IP addresses in menus vice host names */ hist[cur_page].is_ip_addr = is_ip_addr(page.menu[i].host); strcpy(hist[cur_page].host, page.menu[i].host); strcpy(hist[cur_page].selector, page.menu[i].selector); hist[cur_page].port_num = atoi(page.menu[i].port_num); hist[cur_page].item_type = page.menu[i].item_type; if (page.menu[i].item_type == '7') hist[cur_page].is_query = 1; if (hist[cur_page].selector[0] == '\0') hist[cur_page].use_selector = 0; else hist[cur_page].use_selector = 1; reset_page(); goto restart; } else { printf("Error: link number %d is out of range!\n", i); goto get_input; } } else if (input_str[0] == 'g') { /* "go" command with host arg. May be followed by optional port number and selector arguments */ hist_next(); temp_str1 = &input_str[1]; while (*temp_str1 && *temp_str1 == ' ') ++temp_str1; /* skip spaces */ temp_str2 = temp_str1; while (*temp_str1 && *temp_str1 != ' ') ++temp_str1; /* skip to end of host */ *temp_str1 = '\0'; hist[cur_page].is_ip_addr = is_ip_addr(temp_str2); strcpy(hist[cur_page].host, temp_str2); hist[cur_page].port_num = 70; /* many sites reject empty selector even though allowed by RFC1436 */ strcpy(hist[cur_page].selector, "/"); hist[cur_page].use_selector = 1; ++temp_str1; while (*temp_str1 && *temp_str1 == ' ') ++temp_str1; /* skip spaces */ if (*temp_str1 && isdigit(*temp_str1)) { temp = atoi(temp_str1); if (temp > 0) hist[cur_page].port_num = temp; while (*temp_str1 && isdigit(*temp_str1)) ++temp_str1; /* skip to end of port */ } if (*temp_str1) { while (*temp_str1 && *temp_str1 == ' ') ++temp_str1; /* skip spaces */ if (*temp_str1) { /* selector string found, extract it */ strcpy(hist[cur_page].selector, temp_str1); hist[cur_page].use_selector = 1; } } hist[cur_page].item_type = '1'; reset_page(); goto restart; } else if (input_str[0] == 'a') { /* "add" command to add page to favorites menu */ if (hist[cur_page].is_fav_menu) { printf("Cannot add favorites menu to favorites!\n"); goto get_input; } /* "add" command to add page to favorites with optional file name argument to specify the menu file. Default is 'gc-favs.gcm'. */ temp_str1 = &input_str[1]; while (*temp_str1 && *temp_str1 == ' ') ++temp_str1; temp_str2 = temp_str1; while (*temp_str1 && *temp_str1 != ' ') ++temp_str1; *temp_str1 = '\0'; if (temp_str2[0] == '\0') add_to_favorites(DEF_FAVS_FNAME); else add_to_favorites(temp_str2); is_redraw = 1; goto redraw; } else if (input_str[0] == 'v') { /* "view" command to open favorites menu with optional file name argument to specify the menu file. Default is 'gc-favs.gcm'. */ hist_next(); temp_str1 = &input_str[1]; while (*temp_str1 && *temp_str1 == ' ') ++temp_str1; /* skip spaces */ temp_str2 = temp_str1; while (*temp_str1 && *temp_str1 != ' ') ++temp_str1; /* skip to end */ *temp_str1 = '\0'; if (temp_str2[0] == '\0') strcpy(hist[cur_page].selector, DEF_FAVS_FNAME); else strcpy(hist[cur_page].selector, temp_str2); strcpy(hist[cur_page].host, "localhost"); hist[cur_page].port_num = 70; hist[cur_page].item_type = '1'; hist[cur_page].is_ip_addr = 0; hist[cur_page].is_fav_menu = 1; reset_page(); goto favorites; } else if (input_str[0] == 'i' && input_str[1] == '\0') { /* "info" command to print info about currently displayed page */ if (hist[cur_page].is_query) printf("%s:%d %s?%s '%c' (%d bytes)", hist[cur_page].host, hist[cur_page].port_num, hist[cur_page].selector, hist[cur_page].query, hist[cur_page].item_type, page.num_bytes); else printf("%s:%d %s '%c' (%d bytes)", hist[cur_page].host, hist[cur_page].port_num, hist[cur_page].selector, hist[cur_page].item_type, page.num_bytes); if (!hist[cur_page].is_fav_menu) { /* print cache file name */ temp = test_cache(hist[cur_page].host, hist[cur_page].selector, hist[cur_page].query, hist[cur_page].is_query); if (temp != -1) printf(" [%s%06d.gcc]", cfile_dir, temp); else printf("[----------]"); } printf("\n"); goto get_input; } else if (input_str[0] == 'i' && input_str[1] != '\0') { /* "info" command with link number argument */ temp_str1 = &input_str[1]; while (*temp_str1 && *temp_str1 == ' ') ++temp_str1; /* skip spaces */ temp = atoi(temp_str1); if (temp > 0 && temp <= page.max_linknum) { for (i = 0; i < page.num_lines; i++) { if (page.menu[i].link_num == temp) break; } printf("%s:%d %s '%c'", page.menu[i].host, atoi(page.menu[i].port_num), page.menu[i].selector, page.menu[i].item_type); } else { printf("Error: link number %d is out of range!\n", i); goto get_input; } if (page.menu[i].item_type == '0' || page.menu[i].item_type == '1') { /* if menu item is text or menu type, print cache file name */ temp = test_cache(page.menu[i].host, page.menu[i].selector, "", 0); if (temp != -1) printf(" [%s%06d.gcc]", cfile_dir, temp); else printf("[----------]"); } printf("\n"); goto get_input; } else if (input_str[0] == 'c') { /* "cache" command to force caching of page */ if (hist[cur_page].is_fav_menu) { printf("Cannot cache favorites menu!\n"); goto get_input; } /* cache current page; useful when gc not run with cache enabled */ temp = cache_page(cur_page); if (temp != -1) { sprintf(req_buffer, "%s%06d.gcc", cfile_dir, temp); printf("Cache file %s created!\n", req_buffer); hist[cur_page].force_request = 0; } else { printf("Error %d: failed to create cache file %s!\n", errno, req_buffer); } goto get_input; } else if (input_str[0] == 'k') { /* "kill" command to delete cached copy of page */ if (hist[cur_page].is_fav_menu) { printf("Cannot kill favorites menu!\n"); goto get_input; } /* "kill" command to delete cache file from disk */ temp = test_cache(hist[cur_page].host, hist[cur_page].selector, hist[cur_page].query, hist[cur_page].is_query); if (temp != -1) { sprintf(req_buffer, "%s%06d.gcc", cfile_dir, temp); printf("Delete cache file %s. Are you sure? [y/N]\n", req_buffer); get_input_str(input_str, 2); if (input_str[0] != 'y' && input_str[0] != 'Y') { printf("Cache file deletion canceled!\n"); goto get_input; } if (unlink(req_buffer) == -1) printf("Error %d: failed to delete cache file %s!\n", errno, req_buffer); else printf("Cache file %s deleted!\n", req_buffer); } else { printf("No cache file found for this page!\n"); } goto get_input; } else if (input_str[0] == '?') { /* print help text */ printf("q (q)uit d re(d)raw page "); printf("r (r)eload page\n"); printf("h go to (h)ome page b go (b)ack one page "); printf("fN go (f)orward to link N\n"); printf("g host [port] (g)o server i (i)nfo this page "); printf("iN (i)nfo link N\n"); printf("a [file] (a)dd to favs v [file] (v)iew favs "); printf("? print this message\n"); printf("s (s)ave current page sN (s)ave page link N "); printf("k (k)ill current page cache\n"); goto get_input; } else { goto get_input; } end: if (page.menu != NULL) free((MENU_LINE *)page.menu); if (page.buf != NULL) free((char *)page.buf); exit(0); }