cgmnlm

colorful gemini line mode browser
git clone https://git.clttr.info/cgmnlm.git
Log (Feed) | Files | Refs (Tags) | README | LICENSE

client.c (7424B)


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