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 }