gmni

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

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 }