gmni

a gemini line mode client
git clone https://git.clttr.info/gmni.git
Log (Feed) | Files | Refs (Tags) | README | LICENSE

gmni.c (8410B)


      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 		fprintf(stderr,
     87 			"The certificate offered by this server is of unknown trust. "
     88 			"Its fingerprint is: \n"
     89 			"%s\n\n"
     90 			"Use '-j once' to trust temporarily, or '-j always' to add to the trust store.\n", fingerprint);
     91 		break;
     92 	case TOFU_FINGERPRINT_MISMATCH:
     93 		fprintf(stderr,
     94 			"The certificate offered by this server DOES NOT MATCH the one we have on file.\n"
     95 			"/!\\ Someone may be eavesdropping on or manipulating this connection. /!\\\n"
     96 			"The unknown certificate's fingerprint is:\n"
     97 			"%s\n\n"
     98 			"The expected fingerprint is:\n"
     99 			"%s\n\n"
    100 			"If you're certain that this is correct, edit %s:%d\n",
    101 			fingerprint, host->fingerprint,
    102 			cfg->tofu.known_hosts_path, host->lineno);
    103 		return TOFU_FAIL;
    104 	}
    105 
    106 	if (action == TOFU_ASK) {
    107 		return TOFU_FAIL;
    108 	}
    109 
    110 	return action;
    111 }
    112 
    113 static struct gmni_client_certificate *
    114 load_client_cert(char *argv_0, char *path)
    115 {
    116 	char *certpath = strtok(path, ":");
    117 	if (!certpath) {
    118 		usage(argv_0);
    119 		exit(1);
    120 	}
    121 
    122 	FILE *certf = fopen(certpath, "r");
    123 	if (!certf) {
    124 		fprintf(stderr, "Failed to open certificate: %s\n",
    125 				strerror(errno));
    126 		exit(1);
    127 	}
    128 
    129 	char *keypath = strtok(NULL, ":");
    130 	if (!keypath) {
    131 		usage(argv_0);
    132 		exit(1);
    133 	}
    134 
    135 	FILE *keyf = fopen(keypath, "r");
    136 	if (!keyf) {
    137 		fprintf(stderr, "Failed to open certificate: %s\n",
    138 				strerror(errno));
    139 		exit(1);
    140 	}
    141 
    142 	struct gmni_client_certificate *cert =
    143 		calloc(1, sizeof(struct gmni_client_certificate));
    144 	if (gmni_ccert_load(cert, certf, keyf) != 0) {
    145 		fprintf(stderr, "Failed to load client certificate: %s\n",
    146 				strerror(errno));
    147 		exit(1);
    148 	}
    149 	return cert;
    150 }
    151 
    152 int
    153 main(int argc, char *argv[])
    154 {
    155 	enum header_mode {
    156 		OMIT_HEADERS,
    157 		SHOW_HEADERS,
    158 		ONLY_HEADERS,
    159 	};
    160 	enum header_mode header_mode = OMIT_HEADERS;
    161 
    162 	enum input_mode {
    163 		INPUT_READ,
    164 		INPUT_SUPPRESS,
    165 	};
    166 	enum input_mode input_mode = INPUT_READ;
    167 	FILE *input_source = stdin;
    168 
    169 	char *output_file = NULL;
    170 
    171 	bool follow_redirects = false, linefeed = true;
    172 	int max_redirect = 5;
    173 
    174 	struct addrinfo hints = {0};
    175 	struct gemini_options opts = {
    176 		.hints = &hints,
    177 	};
    178 	struct tofu_config cfg;
    179 	cfg.action = TOFU_ASK;
    180 
    181 	int c;
    182 	while ((c = getopt(argc, argv, "46d:D:E:hj:lLiINR:o:")) != -1) {
    183 		switch (c) {
    184 		case '4':
    185 			hints.ai_family = AF_INET;
    186 			break;
    187 		case '6':
    188 			hints.ai_family = AF_INET6;
    189 			break;
    190 		case 'd':
    191 			input_mode = INPUT_READ;
    192 			input_source = fmemopen(optarg, strlen(optarg) + 1, "r");
    193 			break;
    194 		case 'D':
    195 			input_mode = INPUT_READ;
    196 			if (strcmp(optarg, "-") == 0) {
    197 				input_source = stdin;
    198 			} else {
    199 				input_source = fopen(optarg, "r");
    200 				if (!input_source) {
    201 					fprintf(stderr, "Error: open %s: %s",
    202 							optarg, strerror(errno));
    203 					return 1;
    204 				}
    205 			}
    206 			break;
    207 		case 'E':
    208 			opts.client_cert = load_client_cert(argv[0], optarg);
    209 			break;
    210 		case 'h':
    211 			usage(argv[0]);
    212 			return 0;
    213 		case 'j':
    214 			if (strcmp(optarg, "fail") == 0) {
    215 				cfg.action = TOFU_FAIL;
    216 			} else if (strcmp(optarg, "once") == 0) {
    217 				cfg.action = TOFU_TRUST_ONCE;
    218 			} else if (strcmp(optarg, "always") == 0) {
    219 				cfg.action = TOFU_TRUST_ALWAYS;
    220 			} else {
    221 				usage(argv[0]);
    222 				return 1;
    223 			}
    224 			break;
    225 		case 'l':
    226 			linefeed = false;
    227 			break;
    228 		case 'L':
    229 			follow_redirects = true;
    230 			break;
    231 		case 'i':
    232 			header_mode = SHOW_HEADERS;
    233 			break;
    234 		case 'I':
    235 			header_mode = ONLY_HEADERS;
    236 			input_mode = INPUT_SUPPRESS;
    237 			break;
    238 		case 'N':
    239 			input_mode = INPUT_SUPPRESS;
    240 			break;
    241 		case 'R':;
    242 			char *endptr;
    243 			errno = 0;
    244 			max_redirect = strtoul(optarg, &endptr, 10);
    245 			if (*endptr || errno != 0) {
    246 				fprintf(stderr, "Error: -R expects numeric argument\n");
    247 				return 1;
    248 			}
    249 			break;
    250 		case 'o':
    251 			output_file = optarg;
    252 			break;
    253 		default:
    254 			fprintf(stderr, "fatal: unknown flag %c\n", c);
    255 			return 1;
    256 		}
    257 	}
    258 
    259 	if (optind != argc - 1) {
    260 		usage(argv[0]);
    261 		return 1;
    262 	}
    263 
    264 	gemini_tofu_init(&cfg.tofu, &tofu_callback, &cfg);
    265 
    266 	bool exit = false;
    267 	struct Curl_URL *url = curl_url();
    268 
    269 	if (curl_url_set(url, CURLUPART_URL, argv[optind], 0) != CURLUE_OK) {
    270 		// TODO: Better error
    271 		fprintf(stderr, "Error: invalid URL\n");
    272 		return 1;
    273 	}
    274 
    275 	int ret = 0, nredir = 0;
    276 	while (!exit) {
    277 		char *buf = NULL;
    278 		curl_url_get(url, CURLUPART_URL, &buf, 0);
    279 
    280 		struct gemini_response resp;
    281 		enum gemini_result r = gemini_request(buf,
    282 			&opts, &cfg.tofu, &resp);
    283 
    284 		if (r != GEMINI_OK) {
    285 			fprintf(stderr, "Error: %s\n", gemini_strerr(r, &resp));
    286 			ret = (int)r;
    287 			exit = true;
    288 			goto next;
    289 		}
    290 
    291 		if (header_mode != OMIT_HEADERS) {
    292 			printf("%d %s\n", resp.status, resp.meta);
    293 		}
    294 
    295 		switch (gemini_response_class(resp.status)) {
    296 		case GEMINI_STATUS_CLASS_INPUT:
    297 			if (input_mode == INPUT_SUPPRESS) {
    298 				exit = true;
    299 				break;
    300 			}
    301 
    302 			char *input = get_input(&resp, input_source);
    303 			if (!input) {
    304 				r = 1;
    305 				exit = true;
    306 				break;
    307 			}
    308 
    309 			char *new_url = gemini_input_url(buf, input);
    310 			assert(new_url);
    311 
    312 			free(input);
    313 
    314 			curl_url_set(url, CURLUPART_URL, new_url, 0);
    315 			goto next;
    316 		case GEMINI_STATUS_CLASS_REDIRECT:
    317 			if (++nredir >= max_redirect) {
    318 				fprintf(stderr,
    319 					"Error: maximum redirects (%d) exceeded",
    320 					max_redirect);
    321 				exit = true;
    322 				goto next;
    323 			}
    324 
    325 			curl_url_set(url, CURLUPART_URL, resp.meta, 0);
    326 
    327 			if (!follow_redirects) {
    328 				if (header_mode == OMIT_HEADERS) {
    329 					fprintf(stderr, "REDIRECT: %d %s\n",
    330 						resp.status, resp.meta);
    331 				}
    332 				exit = true;
    333 			}
    334 			goto next;
    335 		case GEMINI_STATUS_CLASS_CLIENT_CERTIFICATE_REQUIRED:
    336 			assert(0); // TODO
    337 		case GEMINI_STATUS_CLASS_TEMPORARY_FAILURE:
    338 		case GEMINI_STATUS_CLASS_PERMANENT_FAILURE:
    339 			if (header_mode == OMIT_HEADERS) {
    340 				fprintf(stderr, "%s: %d %s\n",
    341 					resp.status / 10 == 4 ?
    342 					"TEMPORARY FAILURE" : "PERMANENT FAILURE",
    343 					resp.status, resp.meta);
    344 			}
    345 			exit = true;
    346 			break;
    347 		case GEMINI_STATUS_CLASS_SUCCESS:
    348 			exit = true;
    349 			break;
    350 		}
    351 
    352 		if (header_mode != ONLY_HEADERS) {
    353 			if (gemini_response_class(resp.status) !=
    354 					GEMINI_STATUS_CLASS_SUCCESS) {
    355 				break;
    356 			}
    357 
    358 			if (output_file != NULL) {
    359 				fprintf(stderr, "Downloading %s to %s\n", buf,
    360 					output_file);
    361 				if (print_resp(linefeed, resp, output_file, buf)) {
    362 					fprintf(stderr, "Finished download\n");
    363 				}
    364 			} else {
    365 				print_resp_file(linefeed, resp, stdout);
    366 			}
    367 			break;
    368 		}
    369 
    370 next:
    371 		free(buf);
    372 		gemini_response_finish(&resp);
    373 	}
    374 
    375 	curl_url_cleanup(url);
    376 	gemini_tofu_finish(&cfg.tofu);
    377 	return ret;
    378 }