cgmnlm

colorful gemini line mode browser
git clone https://git.clttr.info/cgmnlm.git
Log (Feed) | Files | Refs (Tags) | README | LICENSE

gmni.c (9006B)


      1 #include <assert.h>
      2 #include <bearssl.h>
      3 #include <errno.h>
      4 #include <getopt.h>
      5 #include <netdb.h>
      6 #include <stdbool.h>
      7 #include <stdio.h>
      8 #include <stdlib.h>
      9 #include <string.h>
     10 #include <sys/socket.h>
     11 #include <sys/types.h>
     12 #include <termios.h>
     13 #include <unistd.h>
     14 #include <gmni/certs.h>
     15 #include <gmni/gmni.h>
     16 #include <gmni/tofu.h>
     17 #include <gmni/url.h>
     18 #include "util.h"
     19 
     20 static void
     21 usage(const char *argv_0)
     22 {
     23 	fprintf(stderr,
     24 		"usage: %s [-46lLiIN] [-j mode] [-E cert] [-d input] [-D path] gemini://...\n",
     25 		argv_0);
     26 }
     27 
     28 static char *
     29 get_input(const struct gemini_response *resp, FILE *source)
     30 {
     31 	int r = 0;
     32 	struct termios attrs;
     33 	bool tty = fileno(source) != -1 && isatty(fileno(source));
     34 	char *input = NULL;
     35 	if (tty) {
     36 		fprintf(stderr, "%s: ", resp->meta);
     37 		if (resp->status == GEMINI_STATUS_SENSITIVE_INPUT) {
     38 			r = tcgetattr(fileno(source), &attrs);
     39 			struct termios new_attrs;
     40 			r = tcgetattr(fileno(source), &new_attrs);
     41 			if (r != -1) {
     42 				new_attrs.c_lflag &= ~ECHO;
     43 				tcsetattr(fileno(source), TCSANOW, &new_attrs);
     44 			}
     45 		}
     46 	}
     47 	size_t s = 0;
     48 	ssize_t n = getline(&input, &s, source);
     49 	if (n == -1) {
     50 		fprintf(stderr, "Error reading input: %s\n",
     51 			feof(source) ? "EOF" :
     52 			strerror(ferror(source)));
     53 		return NULL;
     54 	}
     55 	input[n - 1] = '\0'; // Drop LF
     56 	if (tty && resp->status == GEMINI_STATUS_SENSITIVE_INPUT && r != -1) {
     57 		attrs.c_lflag &= ~ECHO;
     58 		tcsetattr(fileno(source), TCSANOW, &attrs);
     59 	}
     60 	return input;
     61 }
     62 
     63 struct tofu_config {
     64 	struct gemini_tofu tofu;
     65 	enum tofu_action action;
     66 };
     67 
     68 static enum tofu_action
     69 tofu_callback(enum tofu_error error, const char *fingerprint,
     70 	struct known_host *host, void *data)
     71 {
     72 	struct tofu_config *cfg = (struct tofu_config *)data;
     73 	enum tofu_action action = cfg->action;
     74 	switch (error) {
     75 	case TOFU_VALID:
     76 		assert(0); // Invariant
     77 	case TOFU_INVALID_CERT:
     78 		fprintf(stderr,
     79 			"The server presented an invalid certificate with fingerprint %s.\n",
     80 			fingerprint);
     81 		if (action == TOFU_TRUST_ALWAYS) {
     82 			action = TOFU_TRUST_ONCE;
     83 		}
     84 		break;
     85 	case TOFU_UNTRUSTED_CERT:
     86 		if (action != TOFU_TRUST_ONCE && action != TOFU_TRUST_ALWAYS) {
     87 			fprintf(stderr,
     88 				"The certificate offered by this server is of unknown trust. "
     89 				"Its fingerprint is: \n"
     90 				"%s\n\n"
     91 				"Use '-j once' to trust temporarily, or '-j always' to add to the trust store.\n", fingerprint);
     92 		}
     93 		break;
     94 	case TOFU_FINGERPRINT_MISMATCH:
     95 		fprintf(stderr,
     96 			"The certificate offered by this server DOES NOT MATCH the one we have on file.\n"
     97 			"/!\\ Someone may be eavesdropping on or manipulating this connection. /!\\\n"
     98 			"The unknown certificate's fingerprint is:\n"
     99 			"%s\n\n"
    100 			"The expected fingerprint is:\n"
    101 			"%s\n\n"
    102 			"If you're certain that this is correct, edit %s:%d\n",
    103 			fingerprint, host->fingerprint,
    104 			cfg->tofu.known_hosts_path, host->lineno);
    105 		return TOFU_FAIL;
    106 	}
    107 
    108 	if (action == TOFU_ASK) {
    109 		return TOFU_FAIL;
    110 	}
    111 
    112 	return action;
    113 }
    114 
    115 static struct gmni_client_certificate *
    116 load_client_cert(char *argv_0, char *path)
    117 {
    118 	char *certpath = strtok(path, ":");
    119 	if (!certpath) {
    120 		usage(argv_0);
    121 		exit(1);
    122 	}
    123 
    124 	FILE *certf = fopen(certpath, "r");
    125 	if (!certf) {
    126 		fprintf(stderr, "Failed to open certificate: %s\n",
    127 				strerror(errno));
    128 		exit(1);
    129 	}
    130 
    131 	char *keypath = strtok(NULL, ":");
    132 	if (!keypath) {
    133 		usage(argv_0);
    134 		exit(1);
    135 	}
    136 
    137 	FILE *keyf = fopen(keypath, "r");
    138 	if (!keyf) {
    139 		fprintf(stderr, "Failed to open certificate: %s\n",
    140 				strerror(errno));
    141 		exit(1);
    142 	}
    143 
    144 	struct gmni_client_certificate *cert =
    145 		calloc(1, sizeof(struct gmni_client_certificate));
    146 	if (gmni_ccert_load(cert, certf, keyf) != 0) {
    147 		fprintf(stderr, "Failed to load client certificate: %s\n",
    148 				strerror(errno));
    149 		exit(1);
    150 	}
    151 	return cert;
    152 }
    153 
    154 int
    155 main(int argc, char *argv[])
    156 {
    157 	enum header_mode {
    158 		OMIT_HEADERS,
    159 		SHOW_HEADERS,
    160 		ONLY_HEADERS,
    161 	};
    162 	enum header_mode header_mode = OMIT_HEADERS;
    163 
    164 	enum input_mode {
    165 		INPUT_READ,
    166 		INPUT_SUPPRESS,
    167 	};
    168 	enum input_mode input_mode = INPUT_READ;
    169 	FILE *input_source = stdin;
    170 
    171 	char *output_file = NULL;
    172 
    173 	bool follow_redirects = false, linefeed = true;
    174 	int max_redirect = 5;
    175 
    176 	struct addrinfo hints = {0};
    177 	struct gemini_options opts = {
    178 		.hints = &hints,
    179 	};
    180 	struct tofu_config cfg;
    181 	cfg.action = TOFU_ASK;
    182 
    183 	int c;
    184 	while ((c = getopt(argc, argv, "46d:D:E:hj:lLiINR:o:")) != -1) {
    185 		switch (c) {
    186 		case '4':
    187 			hints.ai_family = AF_INET;
    188 			break;
    189 		case '6':
    190 			hints.ai_family = AF_INET6;
    191 			break;
    192 		case 'd':
    193 			input_mode = INPUT_READ;
    194 			input_source = fmemopen(optarg, strlen(optarg) + 1, "r");
    195 			break;
    196 		case 'D':
    197 			input_mode = INPUT_READ;
    198 			if (strcmp(optarg, "-") == 0) {
    199 				input_source = stdin;
    200 			} else {
    201 				input_source = fopen(optarg, "r");
    202 				if (!input_source) {
    203 					fprintf(stderr, "Error: open %s: %s",
    204 							optarg, strerror(errno));
    205 					return 1;
    206 				}
    207 			}
    208 			break;
    209 		case 'E':
    210 			opts.client_cert = load_client_cert(argv[0], optarg);
    211 			break;
    212 		case 'h':
    213 			usage(argv[0]);
    214 			return 0;
    215 		case 'j':
    216 			if (strcmp(optarg, "fail") == 0) {
    217 				cfg.action = TOFU_FAIL;
    218 			} else if (strcmp(optarg, "once") == 0) {
    219 				cfg.action = TOFU_TRUST_ONCE;
    220 			} else if (strcmp(optarg, "always") == 0) {
    221 				cfg.action = TOFU_TRUST_ALWAYS;
    222 			} else {
    223 				usage(argv[0]);
    224 				return 1;
    225 			}
    226 			break;
    227 		case 'l':
    228 			linefeed = false;
    229 			break;
    230 		case 'L':
    231 			follow_redirects = true;
    232 			break;
    233 		case 'i':
    234 			header_mode = SHOW_HEADERS;
    235 			break;
    236 		case 'I':
    237 			header_mode = ONLY_HEADERS;
    238 			input_mode = INPUT_SUPPRESS;
    239 			break;
    240 		case 'N':
    241 			input_mode = INPUT_SUPPRESS;
    242 			break;
    243 		case 'R':;
    244 			char *endptr;
    245 			errno = 0;
    246 			max_redirect = strtoul(optarg, &endptr, 10);
    247 			if (*endptr || errno != 0) {
    248 				fprintf(stderr, "Error: -R expects numeric argument\n");
    249 				return 1;
    250 			}
    251 			break;
    252 		case 'o':
    253 			output_file = optarg;
    254 			break;
    255 		default:
    256 			fprintf(stderr, "fatal: unknown flag %c\n", c);
    257 			return 1;
    258 		}
    259 	}
    260 
    261 	if (optind != argc - 1) {
    262 		usage(argv[0]);
    263 		return 1;
    264 	}
    265 
    266 	gemini_tofu_init(&cfg.tofu, &tofu_callback, &cfg);
    267 
    268 	bool exit = false;
    269 	struct Curl_URL *url = curl_url();
    270 
    271 	if (curl_url_set(url, CURLUPART_URL, argv[optind], 0) != CURLUE_OK) {
    272 		// TODO: Better error
    273 		fprintf(stderr, "Error: invalid URL\n");
    274 		return 1;
    275 	}
    276 
    277 	int ret = 0, nredir = 0;
    278 	while (!exit) {
    279 		char *buf;
    280 		curl_url_get(url, CURLUPART_URL, &buf, 0);
    281 
    282 		struct gemini_response resp;
    283 		enum gemini_result r = gemini_request(buf,
    284 			&opts, &cfg.tofu, &resp);
    285 
    286 		free(buf);
    287 
    288 		if (r != GEMINI_OK) {
    289 			fprintf(stderr, "Error: %s\n", gemini_strerr(r, &resp));
    290 			ret = (int)r;
    291 			exit = true;
    292 			goto next;
    293 		}
    294 
    295 		if (header_mode != OMIT_HEADERS) {
    296 			printf("%d %s\n", resp.status, resp.meta);
    297 		}
    298 
    299 		switch (gemini_response_class(resp.status)) {
    300 		case GEMINI_STATUS_CLASS_INPUT:
    301 			if (input_mode == INPUT_SUPPRESS) {
    302 				exit = true;
    303 				break;
    304 			}
    305 
    306 			char *input = get_input(&resp, input_source);
    307 			if (!input) {
    308 				r = 1;
    309 				exit = true;
    310 				break;
    311 			}
    312 
    313 			char *buf;
    314 			curl_url_get(url, CURLUPART_URL, &buf, 0);
    315 
    316 			char *new_url = gemini_input_url(buf, input);
    317 			assert(new_url);
    318 
    319 			free(input);
    320 			free(buf);
    321 
    322 			curl_url_set(url, CURLUPART_URL, new_url, 0);
    323 			goto next;
    324 		case GEMINI_STATUS_CLASS_REDIRECT:
    325 			if (++nredir >= max_redirect) {
    326 				fprintf(stderr,
    327 					"Error: maximum redirects (%d) exceeded",
    328 					max_redirect);
    329 				exit = true;
    330 				goto next;
    331 			}
    332 
    333 			curl_url_set(url, CURLUPART_URL, resp.meta, 0);
    334 
    335 			if (!follow_redirects) {
    336 				if (header_mode == OMIT_HEADERS) {
    337 					fprintf(stderr, "REDIRECT: %d %s\n",
    338 						resp.status, resp.meta);
    339 				}
    340 				exit = true;
    341 			}
    342 			goto next;
    343 		case GEMINI_STATUS_CLASS_CLIENT_CERTIFICATE_REQUIRED:
    344 			assert(0); // TODO
    345 		case GEMINI_STATUS_CLASS_TEMPORARY_FAILURE:
    346 		case GEMINI_STATUS_CLASS_PERMANENT_FAILURE:
    347 			if (header_mode == OMIT_HEADERS) {
    348 				fprintf(stderr, "%s: %d %s\n",
    349 					resp.status / 10 == 4 ?
    350 					"TEMPORARY FAILURE" : "PERMANENT FAILURE",
    351 					resp.status, resp.meta);
    352 			}
    353 			exit = true;
    354 			break;
    355 		case GEMINI_STATUS_CLASS_SUCCESS:
    356 			exit = true;
    357 			break;
    358 		}
    359 
    360 		if (header_mode != ONLY_HEADERS) {
    361 			if (gemini_response_class(resp.status) !=
    362 					GEMINI_STATUS_CLASS_SUCCESS) {
    363 				break;
    364 			}
    365 
    366 			if (output_file != NULL) {
    367 				char *buf;
    368 				curl_url_get(url, CURLUPART_URL, &buf, 0);
    369 
    370 				ret = download_resp(stderr, resp, output_file, buf);
    371 				free(buf);
    372 
    373 				break;
    374 			}
    375 
    376 			char last = 0;
    377 			char buf[BUFSIZ];
    378 			for (int n = 1; n > 0;) {
    379 				n = br_sslio_read(&resp.body, buf, BUFSIZ);
    380 				if (n > 0) {
    381 					last = buf[n - 1];
    382 				}
    383 				ssize_t w = 0;
    384 				while (w < (ssize_t)n) {
    385 					ssize_t x = fwrite(&buf[w], 1, n - w, stdout);
    386 					if (ferror(stdout)) {
    387 						fprintf(stderr, "Error: write: %s\n",
    388 							strerror(errno));
    389 						return 1;
    390 					}
    391 					w += x;
    392 				}
    393 			}
    394 			if (strncmp(resp.meta, "text/", 5) == 0
    395 					&& linefeed && last != '\n'
    396 					&& isatty(STDOUT_FILENO)) {
    397 				printf("\n");
    398 			}
    399 			break;
    400 		}
    401 
    402 next:
    403 		gemini_response_finish(&resp);
    404 	}
    405 
    406 	curl_url_cleanup(url);
    407 	gemini_tofu_finish(&cfg.tofu);
    408 	return ret;
    409 }