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 }