├── .gitignore ├── .editorconfig ├── cgorc ├── Makefile ├── LICENSE ├── README.md ├── cgo.1 └── cgo.c /.gitignore: -------------------------------------------------------------------------------- 1 | # ignore build artifacts 2 | *.o 3 | cgo 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | # Unix-style newlines with a newline ending every file 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | charset = utf-8 9 | indent_style = space 10 | indent_size = 4 11 | 12 | # Tab indentation (no size specified) 13 | [Makefile] 14 | indent_style = tab 15 | -------------------------------------------------------------------------------- /cgorc: -------------------------------------------------------------------------------- 1 | # start URI 2 | start_uri gopher://gopher.floodgap.com:70/ 3 | 4 | # commands 5 | cmd_text less 6 | cmd_browser firefox 7 | cmd_image display 8 | cmd_player mplayer 9 | 10 | # colors 11 | color_prompt 1;34 12 | color_selector 1;32 13 | 14 | # be "verbose" 15 | verbose off 16 | 17 | # bookmarks 18 | bookmark1 gopher://gopher.floodgap.com:70/ 19 | bookmark2 gopher://devio.us:70/~steini 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # see cgo.c for copyright and license details 2 | PREFIX = /usr/local 3 | MANPREFIX = $(PREFIX)/share/man 4 | CC = cc 5 | CFLAGS ?= -O2 -Wall 6 | OBJ = cgo.o 7 | BIN = cgo 8 | 9 | default: $(OBJ) 10 | $(CC) $(CFLAGS) $(LDFLAGS) -o $(BIN) $(OBJ) 11 | 12 | clean: 13 | rm -f $(OBJ) $(BIN) 14 | 15 | install: default 16 | @mkdir -p $(DESTDIR)$(PREFIX)/bin/ 17 | @install $(BIN) $(DESTDIR)$(PREFIX)/bin/${BIN} 18 | @mkdir -p $(DESTDIR)$(MANPREFIX)/man1 19 | @cp cgo.1 $(DESTDIR)$(MANPREFIX)/man1/cgo.1 20 | @chmod 644 $(DESTDIR)$(MANPREFIX)/man1/cgo.1 21 | 22 | uninstall: 23 | @rm -f $(DESTDIR)$(PREFIX)/bin/$(BIN) 24 | @rm -r $(DESTDIR)$(MANPREFIX)/man1/cgo.1 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | cgo - a simple terminal based gopher client 2 | Copyright (c) 2019 Sebastian Steinhauer 3 | 4 | Permission to use, copy, modify, and distribute this software for any 5 | purpose with or without fee is hereby granted, provided that the above 6 | copyright notice and this permission notice appear in all copies. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | cgo - a terminal gopher client 2 | ============================== 3 | 4 | Summary 5 | ------- 6 | 7 | cgo is a UNIX/Linux terminal based gopher client. It has no other 8 | dependencies than libc and some syscalls. It should run on every 9 | VT100 compatible terminal. To show media like images, music or 10 | webpages it relies on external programs you can specify. 11 | 12 | 13 | What does cgo mean? 14 | ------------------- 15 | 16 | cgo means more or less, the "c go"pher client. And c could 17 | stand for C (the programming language), colorful or console. 18 | You may choose one of the meanings or propose other :) 19 | (but please not crappy!) 20 | 21 | 22 | How to install 23 | -------------- 24 | 25 | Grab the source code, open cgo.c and adjust the external programs 26 | to your needs. You can also change the default gopherhole where 27 | cgo connects on startup without any parameters (e.g. you can 28 | tell cgo to connect directly to some Veronica search engine). 29 | And if you don't like the default colors, you're able to change 30 | them also here. 31 | If you're done with changing the defaults, just "make" the 32 | final binary. 33 | 34 | 35 | Parameters 36 | ---------- 37 | 38 | In case you omit all parameters cgo will show you the default 39 | gopherhole specified in the source file. 40 | 41 | * -H show usage 42 | * -v print version 43 | * gopher URI opens the given gopher URI 44 | 45 | 46 | Usage 47 | ----- 48 | 49 | When "surfing" in the gopherspace cgo only presents you 50 | with directory listings. Every selector is preceeded with two 51 | ascii chars, or three if we run out of selectors in the 52 | range 'aa', 'ab' ... 'zz'. By typing in these chars cgo will 53 | jump to the given selector. Every time you jump to another 54 | directory listing cgo generates a history entry (like every 55 | browser). To show other media cgo uses external programs 56 | to present it (e.g. less, display, mplayer, firefox). 57 | The following commands are understood by cgo: 58 | 59 | * ? help 60 | * < jump back in history 61 | * * reload directory 62 | * [link] show / jump to selector 63 | * .[link] download selector 64 | * H show history 65 | * H[link] jump to specified history item 66 | * G[URI] jumps right to the specified gopher URI 67 | * B show bookmarks 68 | * B[link] jump to specified bookmark item 69 | 70 | [link] stands for the two (or three) colored letters in front of selectors. 71 | 72 | Configuration 73 | ------------- 74 | 75 | cgo reads `/etc/cgorc` and then `$(HOME)/.cgorc` for defaults. If both 76 | files are missing, hardcoded defaults will be used. Following configuration 77 | keys are recognized by cgo: 78 | 79 | * `start_uri` the gopher URI which is displayed at start 80 | * `cmd_text` command to show text files 81 | * `cmd_browser` command to HTML links 82 | * `cmd_image` command to show images 83 | * `cmd_player` command to play audio files 84 | * `color_prompt` ANSI color sequence for the prompt 85 | * `color_selector` ANSI color sequence for selectors 86 | * `verbose` If not "false" or "off" it will show messages like "downloading" / "executing" when downloading a selector 87 | * `bookmarkN` configure bookmarks 88 | 89 | Todo 90 | ---- 91 | 92 | * list is empty :D 93 | 94 | 95 | Bugs 96 | ---- 97 | 98 | * none I'm aware of :) 99 | 100 | 101 | Feel free to use this small gopher client. I hope you'll 102 | find it as useful as I do. Send me comments or patches if you 103 | like. I would appreciate it. 104 | -------------------------------------------------------------------------------- /cgo.1: -------------------------------------------------------------------------------- 1 | .\" 2 | .\" cgo - a simple terminal based gopher client 3 | .\" Copyright (c) 2013-2019 Sebastian Steinhauer 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 | .Dd Mai 31, 2019 18 | .Dt CGO 1 19 | .Os 20 | .Sh Name 21 | .Nm cgo 22 | .Nd simple terminal based gopher client 23 | .Sh SYNOPSIS 24 | .Nm cgo 25 | .Op Fl Hv 26 | .Op Ar gopher URI 27 | .Sh DESCRIPTION 28 | .Nm 29 | is a UNIX/Linux terminal based gopher client. 30 | It has no dependencies other than libc and some syscalls. 31 | It should run on every VT100 compatible terminal. 32 | To show media like images, music, or web pages it relies on external programs 33 | you can specify. 34 | .Pp 35 | .Nm 36 | means, more or less, the "c go"pher client. 37 | And c could stand for C (the programming language), colorful, or console. 38 | .Pp 39 | The options are as follows: 40 | .Bl -tag -width Ds 41 | .It Fl H 42 | Show usage. 43 | .It Fl v 44 | Print version. 45 | .It Ar gopher URI 46 | Open given gopher URI. 47 | .El 48 | .Pp 49 | When surfing gopherspace 50 | .Nm 51 | only presents directory listings. 52 | Every selector is preceded by two ASCII characters, 53 | or three if we run out of selectors in the range 'aa', 'ab' ... 'zz'. 54 | By typing in these characters 55 | .Nm 56 | will jump to the given selector. 57 | Every time you jump to another directory listing 58 | .Nm 59 | generates a history entry (like every browser). 60 | To show other media 61 | .Nm 62 | uses external programs (e.g. less, display, mplayer, firefox). 63 | .Pp 64 | The following commands are understood by 65 | .Nm : 66 | .Bl -tag -width Ds -compact -offset indent 67 | .It Ar \? 68 | Help. 69 | .It Ar < 70 | Jump back one step in history. 71 | .It Ar * 72 | Reload current directory. 73 | .It Ar H 74 | Show history. 75 | .It Ar B 76 | Show bookmarks. 77 | .It Ar [LINK] 78 | Jump to selector. 79 | .It Ar \.[LINK] 80 | Download selector. 81 | .It Ar H[LINK] 82 | Jump to specified history item. 83 | .It Ar B[LINK] 84 | Jump to specified bookmark item. 85 | .It Ar G[URI] 86 | Jump to the specified gopher URI. 87 | .It Ar CTRL-d 88 | Quit. 89 | .El 90 | .Pp 91 | [LINK] stands for the two (or three) colored letters in front of each selector. 92 | .Sh CONFIGURATION 93 | .Nm 94 | reads /etc/cgorc and then ~/.cgorc for defaults. 95 | If both files are missing, hardcoded defaults will be used. 96 | The following configuration keys are recognized by 97 | .Nm : 98 | .Bl -tag -width Ds -compact -offset indent 99 | .It start_uri 100 | Gopher URI to display at launch. 101 | .It bookmarkN 102 | Configure a bookmark. 103 | .It cmd_text 104 | Program to view text files. 105 | .It cmd_browser 106 | Program to view HTML links. 107 | .It cmd_image 108 | Program to view images. 109 | .It cmd_player 110 | Program to play audio files. 111 | .It color_prompt 112 | ANSI color sequence for the prompt. 113 | .It color_selector 114 | ANSI color sequence for selectors. 115 | .It verbose 116 | If not "false" or "off" it will show messages like "downloading" / "executing" when downloading a selector. 117 | .El 118 | .Sh AUTHOR 119 | .Nm 120 | was written by 121 | .An Sebastian Steinhauer Aq Mt s.steinhauer@yahoo.de . 122 | -------------------------------------------------------------------------------- /cgo.c: -------------------------------------------------------------------------------- 1 | /* 2 | * cgo - a simple terminal based gopher client 3 | * Copyright (c) 2019 Sebastian Steinhauer 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 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 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.1 Copyright (c) 2020 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/1%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 (ignore slash and selector type) */ 762 | if (*uri) ++uri; 763 | if (*uri) ++uri; 764 | for (i = 0; *uri && i < sizeof(parsed_selector) - 1; ++uri, ++i) 765 | parsed_selector[i] = *uri; 766 | parsed_selector[i] = '\0'; 767 | 768 | return 1; 769 | } 770 | 771 | int main(int argc, char *argv[]) 772 | { 773 | int i; 774 | char line[1024], *uri; 775 | 776 | /* copy defaults */ 777 | init_config(); 778 | uri = &config.start_uri[0]; 779 | 780 | /* parse command line */ 781 | for (i = 1; i < argc; i++) { 782 | if (argv[i][0] == '-') switch(argv[i][1]) { 783 | case 'H': 784 | usage(); 785 | break; 786 | case 'v': 787 | banner(stdout); 788 | exit(EXIT_SUCCESS); 789 | default: 790 | usage(); 791 | } else { 792 | uri = argv[i]; 793 | } 794 | } 795 | 796 | /* parse uri */ 797 | if (! parse_uri(uri)) { 798 | banner(stderr); 799 | fprintf(stderr, "invalid gopher URI: %s", argv[i]); 800 | exit(EXIT_FAILURE); 801 | } 802 | 803 | /* main loop */ 804 | view_directory(parsed_host, parsed_port, parsed_selector, 0); 805 | for (;;) { 806 | printf("\033[%sm%s:%s%s\033[0m ", config.color_prompt, 807 | current_host, current_port, current_selector); 808 | fflush(stdout); /* to display the prompt */ 809 | if (! read_line(0, line, sizeof(line))) { 810 | puts("QUIT"); 811 | return EXIT_SUCCESS; 812 | } 813 | i = strlen(line); 814 | switch (line[0]) { 815 | case '?': 816 | puts( 817 | "? - help\n" 818 | "* - reload directory\n" 819 | "< - go back in history\n" 820 | ".[LINK] - download the given link\n" 821 | "H - show history\n" 822 | "H[LINK] - jump to the specified history item\n" 823 | "G[URI] - jump to the given gopher URI\n" 824 | "B - show bookmarks\n" 825 | "B[LINK] - jump to the specified bookmark item\n" 826 | "C^d - quit"); 827 | break; 828 | case '<': 829 | pop_history(); 830 | break; 831 | case '*': 832 | view_directory(current_host, current_port, 833 | current_selector, 0); 834 | break; 835 | case '.': 836 | download_link(make_key(line[1], line[2], line[3])); 837 | break; 838 | case 'H': 839 | if (i == 1 || i == 3 || i == 4) view_history(make_key(line[1], line[2], line[3])); 840 | break; 841 | case 'G': 842 | if (parse_uri(&line[1])) view_directory(parsed_host, parsed_port, parsed_selector, 1); 843 | else puts("invalid gopher URI"); 844 | break; 845 | case 'B': 846 | if (i == 1 || i == 3 || i == 4) view_bookmarks(make_key(line[1], line[2], line[3])); 847 | break; 848 | default: 849 | follow_link(make_key(line[0], line[1], line[2])); 850 | break; 851 | } 852 | } 853 | return EXIT_SUCCESS; /* never get's here but stops cc complaining */ 854 | } 855 | --------------------------------------------------------------------------------