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 }