client.c (9100B)
1 #include <assert.h> 2 #include <errno.h> 3 #include <fcntl.h> 4 #include <netdb.h> 5 #include <bearssl.h> 6 #include <stdlib.h> 7 #include <stdio.h> 8 #include <string.h> 9 #include <sys/socket.h> 10 #include <sys/stat.h> 11 #include <sys/types.h> 12 #include <unistd.h> 13 #include <gmni/certs.h> 14 #include <gmni/gmni.h> 15 #include <gmni/tofu.h> 16 #include <gmni/url.h> 17 18 static enum gemini_result 19 gemini_get_addrinfo(struct Curl_URL *uri, struct gemini_options *options, 20 struct gemini_response *resp, struct addrinfo **addr) 21 { 22 int port = 1965; 23 char *uport; 24 if (curl_url_get(uri, CURLUPART_PORT, &uport, 0) == CURLUE_OK) { 25 port = (int)strtol(uport, NULL, 10); 26 free(uport); 27 } 28 29 if (options && options->addr && options->addr->ai_family != AF_UNSPEC) { 30 *addr = options->addr; 31 } else { 32 struct addrinfo hints = {0}; 33 if (options && options->hints) { 34 hints = *options->hints; 35 } else { 36 hints.ai_family = AF_UNSPEC; 37 } 38 hints.ai_socktype = SOCK_STREAM; 39 40 char pbuf[7]; 41 snprintf(pbuf, sizeof(pbuf), "%d", port); 42 43 char *domain; 44 CURLUcode uc = curl_url_get(uri, CURLUPART_HOST, &domain, 0); 45 assert(uc == CURLUE_OK); 46 47 int r = getaddrinfo(domain, pbuf, &hints, addr); 48 free(domain); 49 if (r != 0) { 50 resp->status = r; 51 return GEMINI_ERR_RESOLVE; 52 } 53 } 54 55 return GEMINI_OK; 56 } 57 58 static enum gemini_result 59 gemini_connect(struct Curl_URL *uri, struct gemini_options *options, 60 struct gemini_response *resp, int *sfd) 61 { 62 struct addrinfo *addr; 63 enum gemini_result res = gemini_get_addrinfo(uri, options, resp, &addr); 64 if (res != GEMINI_OK) { 65 return res; 66 } 67 68 struct addrinfo *rp; 69 for (rp = addr; rp != NULL; rp = rp->ai_next) { 70 *sfd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); 71 if (*sfd == -1) { 72 continue; 73 } 74 if (connect(*sfd, rp->ai_addr, rp->ai_addrlen) != -1) { 75 break; 76 } 77 close(*sfd); 78 } 79 if (rp == NULL) { 80 resp->status = errno; 81 res = GEMINI_ERR_CONNECT; 82 return res; 83 } 84 85 if (!options || !options->addr) { 86 freeaddrinfo(addr); 87 } 88 return res; 89 } 90 91 #define GEMINI_META_MAXLEN 1024 92 #define GEMINI_STATUS_MAXLEN 2 93 94 static int 95 sock_read(void *ctx, unsigned char *buf, size_t len) 96 { 97 for (;;) { 98 ssize_t rlen; 99 rlen = read(*(int *)ctx, buf, len); 100 if (rlen <= 0) { 101 if (rlen < 0 && errno == EINTR) { 102 continue; 103 } 104 return -1; 105 } 106 return (int)rlen; 107 } 108 } 109 110 static int 111 sock_write(void *ctx, const unsigned char *buf, size_t len) 112 { 113 for (;;) { 114 ssize_t wlen; 115 wlen = write(*(int *)ctx, buf, len); 116 if (wlen <= 0) { 117 if (wlen < 0 && errno == EINTR) { 118 continue; 119 } 120 return -1; 121 } 122 return (int)wlen; 123 } 124 } 125 126 static bool 127 has_suffix(const char *str, char *suff) 128 { 129 size_t suffl = strlen(suff); 130 size_t strl = strlen(str); 131 if (strl < suffl) { 132 return false; 133 } 134 return strcmp(&str[strl - suffl], suff) == 0; 135 } 136 137 static enum gemini_result 138 file_request(const char *path, struct gemini_response *resp) 139 { 140 assert(path); 141 assert(resp); 142 resp->meta = NULL; 143 resp->body = NULL; 144 enum gemini_result res = GEMINI_OK; 145 146 int fd = open(path, O_RDONLY); 147 if (fd < 0) { 148 resp->status = GEMINI_STATUS_NOT_FOUND; 149 resp->meta = strdup(strerror(errno)); 150 goto out; 151 } 152 153 if (has_suffix(path, ".gmi") || has_suffix(path, ".gemini")) { 154 resp->meta = strdup("text/gemini"); 155 } else if (has_suffix(path, ".txt")) { 156 resp->meta = strdup("text/plain"); 157 } else { 158 // TODO: return x-octet-stream only if file contents aren't utf-8 159 resp->meta = strdup("application/x-octet-stream"); 160 } 161 162 ssize_t nread = 0; 163 size_t sz = 1; // for NUL 164 size_t len = 0; 165 do { 166 len += nread; 167 sz += BUFSIZ; 168 resp->body = realloc(resp->body, sz); 169 } while ((nread = read(fd, &resp->body[len], BUFSIZ)) > 0); 170 resp->body[len] = '\0'; 171 172 int e = errno; 173 close(fd); 174 if (nread < 0) { 175 free(resp->meta); 176 free(resp->body); 177 resp->meta = NULL; 178 resp->body = NULL; 179 if (e == EISDIR) { 180 resp->status = GEMINI_STATUS_BAD_REQUEST; 181 resp->meta = strdup(strerror(e)); 182 } else { 183 res = GEMINI_ERR_IO; 184 } 185 goto out; 186 } 187 188 resp->status = GEMINI_STATUS_SUCCESS; 189 out: 190 return res; 191 } 192 193 enum gemini_result 194 gemini_request(const char *url, struct gemini_options *options, 195 struct gemini_tofu *tofu, struct gemini_response *resp) 196 { 197 br_sslio_context sctx = {0}; 198 br_ssl_client_context *sc = NULL; 199 200 assert(url); 201 assert(resp); 202 *resp = (struct gemini_response) {0}; 203 if (strlen(url) > 1024) { 204 return GEMINI_ERR_INVALID_URL; 205 } 206 207 struct Curl_URL *uri = curl_url(); 208 if (!uri) { 209 return GEMINI_ERR_OOM; 210 } 211 212 char *scheme = NULL, *host = NULL; 213 enum gemini_result res = GEMINI_OK; 214 if (curl_url_set(uri, CURLUPART_URL, url, 0) != CURLUE_OK) { 215 res = GEMINI_ERR_INVALID_URL; 216 goto cleanup; 217 } 218 219 if (curl_url_get(uri, CURLUPART_SCHEME, &scheme, 0) != CURLUE_OK) { 220 res = GEMINI_ERR_INVALID_URL; 221 goto cleanup; 222 } else if (strcmp(scheme, "file") == 0) { 223 char *path = NULL; 224 curl_url_get(uri, CURLUPART_PATH, &path, 0); 225 res = file_request(path, resp); 226 free(path); 227 goto cleanup; 228 } else if (strcmp(scheme, "gemini") != 0) { 229 res = GEMINI_ERR_NOT_GEMINI; 230 goto cleanup; 231 } 232 233 if (curl_url_get(uri, CURLUPART_HOST, &host, 0) != CURLUE_OK) { 234 res = GEMINI_ERR_INVALID_URL; 235 goto cleanup; 236 } 237 238 int r, sfd; 239 res = gemini_connect(uri, options, resp, &sfd); 240 if (res != GEMINI_OK) { 241 goto cleanup; 242 } 243 244 // TODO: session reuse 245 sc = &tofu->sc; 246 if (options->client_cert) { 247 struct gmni_client_certificate *cert = options->client_cert; 248 struct gmni_private_key *key = cert->key; 249 switch (key->type) { 250 case BR_KEYTYPE_RSA: 251 br_ssl_client_set_single_rsa(sc, 252 cert->chain, cert->nchain, &key->rsa, 253 br_rsa_pkcs1_sign_get_default()); 254 break; 255 case BR_KEYTYPE_EC: 256 br_ssl_client_set_single_ec(sc, 257 cert->chain, cert->nchain, &key->ec, 258 BR_KEYTYPE_SIGN, 0, 259 br_ec_get_default(), 260 br_ecdsa_sign_asn1_get_default()); 261 break; 262 } 263 } else { 264 br_ssl_client_set_client_certificate(sc, NULL); 265 } 266 br_ssl_client_reset(sc, host, 0); 267 268 br_sslio_init(&sctx, &sc->eng, sock_read, &sfd, sock_write, &sfd); 269 270 char req[1024 + 3]; 271 r = snprintf(req, sizeof(req), "%s\r\n", url); 272 assert(r > 0); 273 274 br_sslio_write_all(&sctx, req, r); 275 br_sslio_flush(&sctx); 276 277 // The SSL engine maintains an internal buffer, so this shouldn't be as 278 // inefficient as it looks. It's necessary to do this one byte at a time 279 // to avoid consuming any of the response body buffer. 280 char buf[GEMINI_META_MAXLEN 281 + GEMINI_STATUS_MAXLEN 282 + 2 /* CRLF */ + 1 /* NUL */]; 283 memset(buf, 0, sizeof(buf)); 284 size_t l; 285 for (l = 0; l < 2 || memcmp(&buf[l-2], "\r\n", 2) != 0; ++l) { 286 r = br_sslio_read(&sctx, &buf[l], 1); 287 if (r < 0) { 288 break; 289 } 290 } 291 292 int nread = 0; 293 size_t sz = 1; // for NUL 294 size_t len = 0; 295 do { 296 len += nread; 297 sz += BUFSIZ; 298 resp->body = realloc(resp->body, sz); 299 } while ((nread = br_sslio_read(&sctx, &resp->body[len], BUFSIZ)) > 0); 300 resp->body[len] = '\0'; 301 302 close(sfd); 303 304 int err = br_ssl_engine_last_error(&sc->eng); 305 if (err != 0) { 306 // TODO: Bubble this up properly 307 fprintf(stderr, "SSL error %d\n", err); 308 goto ssl_error; 309 } 310 311 if (l < 3 || strcmp(&buf[l-2], "\r\n") != 0) { 312 fprintf(stderr, "invalid line '%s'\n", buf); 313 res = GEMINI_ERR_PROTOCOL; 314 goto cleanup; 315 } 316 317 char *endptr; 318 resp->status = (enum gemini_status)strtol(buf, &endptr, 10); 319 if (*endptr != ' ' || resp->status < 10 || (int)resp->status >= 70) { 320 fprintf(stderr, "invalid status\n"); 321 res = GEMINI_ERR_PROTOCOL; 322 goto cleanup; 323 } 324 resp->meta = calloc(l - 5 /* 2 digits, space, and CRLF */ + 1 /* NUL */, 1); 325 strncpy(resp->meta, &endptr[1], l - 5); 326 resp->meta[l - 5] = '\0'; 327 328 cleanup: 329 free(host); 330 free(scheme); 331 curl_url_cleanup(uri); 332 if (sc) br_sslio_close(&sctx); 333 return res; 334 ssl_error: 335 res = GEMINI_ERR_SSL; 336 resp->status = r; 337 goto cleanup; 338 } 339 340 void 341 gemini_response_finish(struct gemini_response *resp) 342 { 343 if (!resp) { 344 return; 345 } 346 347 free(resp->meta); 348 free(resp->body); 349 350 resp->meta = NULL; 351 resp->body = NULL; 352 } 353 354 const char * 355 gemini_strerr(enum gemini_result r, struct gemini_response *resp) 356 { 357 switch (r) { 358 case GEMINI_OK: 359 return "OK"; 360 case GEMINI_ERR_OOM: 361 return "Out of memory"; 362 case GEMINI_ERR_INVALID_URL: 363 return "Invalid URL"; 364 case GEMINI_ERR_NOT_GEMINI: 365 return "Not a gemini URL"; 366 case GEMINI_ERR_RESOLVE: 367 return gai_strerror(resp->status); 368 case GEMINI_ERR_CONNECT: 369 return strerror(errno); 370 case GEMINI_ERR_SSL: 371 // TODO: more specific 372 return "SSL error"; 373 case GEMINI_ERR_SSL_VERIFY: 374 // TODO: more specific 375 return "X.509 certificate not trusted"; 376 case GEMINI_ERR_IO: 377 return "I/O error"; 378 case GEMINI_ERR_PROTOCOL: 379 return "Protocol error"; 380 } 381 assert(0); 382 } 383 384 char * 385 gemini_input_url(const char *url, const char *input) 386 { 387 char *new_url = NULL; 388 struct Curl_URL *uri = curl_url(); 389 if (!uri) { 390 return NULL; 391 } 392 if (curl_url_set(uri, CURLUPART_URL, url, 0) != CURLUE_OK) { 393 goto cleanup; 394 } 395 if (curl_url_set(uri, CURLUPART_QUERY, input, CURLU_URLENCODE) != CURLUE_OK) { 396 goto cleanup; 397 } 398 if (curl_url_get(uri, CURLUPART_URL, &new_url, 0) != CURLUE_OK) { 399 new_url = NULL; 400 goto cleanup; 401 } 402 cleanup: 403 curl_url_cleanup(uri); 404 return new_url; 405 } 406 407 enum gemini_status_class 408 gemini_response_class(enum gemini_status status) 409 { 410 return status / 10 * 10; 411 }