From 6f704872d7123566db0e762dcbb62f451d331c73 Mon Sep 17 00:00:00 2001 From: claudio Date: Wed, 1 Sep 2021 08:09:41 +0000 Subject: [PATCH] Add http_proxy support to rpki-client's http handler. OK tb@ --- usr.sbin/rpki-client/encoding.c | 32 ++- usr.sbin/rpki-client/extern.h | 5 +- usr.sbin/rpki-client/http.c | 376 +++++++++++++++++++++++++++++--- 3 files changed, 381 insertions(+), 32 deletions(-) diff --git a/usr.sbin/rpki-client/encoding.c b/usr.sbin/rpki-client/encoding.c index ab8416d61cd..b3dd38c208c 100644 --- a/usr.sbin/rpki-client/encoding.c +++ b/usr.sbin/rpki-client/encoding.c @@ -1,4 +1,4 @@ -/* $OpenBSD: encoding.c,v 1.2 2021/04/19 17:04:35 deraadt Exp $ */ +/* $OpenBSD: encoding.c,v 1.3 2021/09/01 08:09:41 claudio Exp $ */ /* * Copyright (c) 2020 Claudio Jeker * @@ -64,6 +64,36 @@ fail: return -1; } +int +base64_encode(const unsigned char *in, size_t inlen, char **out) +{ + static EVP_ENCODE_CTX *ctx; + unsigned char *to; + int tolen; + + if (ctx == NULL && (ctx = EVP_ENCODE_CTX_new()) == NULL) + err(1, "EVP_ENCODE_CTX_new"); + + *out = NULL; + + if (inlen >= INT_MAX / 2) + return -1; + tolen = ((inlen + 2) / 3) * 4 + 1; + if ((to = malloc(tolen)) == NULL) + return -1; + + EVP_EncodeInit(ctx); + if (EVP_EncodeUpdate(ctx, to, &tolen, in, inlen) != 1) + goto fail; + EVP_EncodeFinal(ctx, to + tolen, &tolen); + *out = to; + return 0; + +fail: + free(to); + return -1; +} + /* * Convert binary buffer of size dsz into an upper-case hex-string. * Returns pointer to the newly allocated string. Function can't fail. diff --git a/usr.sbin/rpki-client/extern.h b/usr.sbin/rpki-client/extern.h index b1e083772eb..b3e14515d9e 100644 --- a/usr.sbin/rpki-client/extern.h +++ b/usr.sbin/rpki-client/extern.h @@ -1,4 +1,4 @@ -/* $OpenBSD: extern.h,v 1.65 2021/07/13 18:39:39 job Exp $ */ +/* $OpenBSD: extern.h,v 1.66 2021/09/01 08:09:41 claudio Exp $ */ /* * Copyright (c) 2019 Kristaps Dzonsons * @@ -364,8 +364,6 @@ extern int verbose; /* Routines for RPKI entities. */ -int base64_decode(const unsigned char *, unsigned char **, - size_t *); void tal_buffer(struct ibuf *, const struct tal *); void tal_free(struct tal *); struct tal *tal_parse(const char *, char *); @@ -499,6 +497,7 @@ void cryptoerrx(const char *, ...) int base64_decode(const unsigned char *, unsigned char **, size_t *); +int base64_encode(const unsigned char *, size_t, char **); char *hex_encode(const unsigned char *, size_t); diff --git a/usr.sbin/rpki-client/http.c b/usr.sbin/rpki-client/http.c index 7773ad602ac..e528d25bdae 100644 --- a/usr.sbin/rpki-client/http.c +++ b/usr.sbin/rpki-client/http.c @@ -1,4 +1,4 @@ -/* $OpenBSD: http.c,v 1.36 2021/08/09 10:30:23 claudio Exp $ */ +/* $OpenBSD: http.c,v 1.37 2021/09/01 08:09:41 claudio Exp $ */ /* * Copyright (c) 2020 Nils Fisher * Copyright (c) 2020 Claudio Jeker @@ -70,6 +70,7 @@ #define HTTP_USER_AGENT "OpenBSD rpki-client" #define HTTP_BUF_SIZE (32 * 1024) #define HTTP_IDLE_TIMEOUT 10 +#define HTTP_IO_TIMEOUT (3 * 60) #define MAX_CONNECTIONS 64 #define NPFDS (MAX_CONNECTIONS + 1) @@ -83,6 +84,9 @@ enum http_state { STATE_FREE, STATE_CONNECT, STATE_TLSCONNECT, + STATE_PROXY_REQUEST, + STATE_PROXY_STATUS, + STATE_PROXY_RESPONSE, STATE_REQUEST, STATE_RESPONSE_STATUS, STATE_RESPONSE_HEADER, @@ -96,9 +100,9 @@ enum http_state { struct http_proxy { char *proxyhost; - char *proxyuser; - char *proxypw; -}; + char *proxyport; + char *proxyauth; +} proxy; struct http_connection { LIST_ENTRY(http_connection) entry; @@ -116,6 +120,7 @@ struct http_connection { size_t bufpos; off_t iosz; time_t idle_time; + time_t io_time; int status; int fd; int chunked; @@ -177,10 +182,13 @@ static enum res http_handle(struct http_connection *); /* Internal state functions used by the above functions */ static enum res http_finish_connect(struct http_connection *); +static enum res proxy_connect(struct http_connection *); static enum res http_tls_connect(struct http_connection *); static enum res http_tls_handshake(struct http_connection *); static enum res http_read(struct http_connection *); static enum res http_write(struct http_connection *); +static enum res proxy_read(struct http_connection *); +static enum res proxy_write(struct http_connection *); static enum res data_write(struct http_connection *); static time_t @@ -277,6 +285,139 @@ url_encode(const char *path) return (epath); } +static char +hextochar(const char *str) +{ + unsigned char c, ret; + + c = str[0]; + ret = c; + if (isalpha(c)) + ret -= isupper(c) ? 'A' - 10 : 'a' - 10; + else + ret -= '0'; + ret *= 16; + + c = str[1]; + ret += c; + if (isalpha(c)) + ret -= isupper(c) ? 'A' - 10 : 'a' - 10; + else + ret -= '0'; + return ret; +} + +static char * +url_decode(const char *str) +{ + char *ret, c; + int i, reallen; + + if (str == NULL) + return NULL; + if ((ret = malloc(strlen(str) + 1)) == NULL) + err(1, "Can't allocate memory for URL decoding"); + for (i = 0, reallen = 0; str[i] != '\0'; i++, reallen++, ret++) { + c = str[i]; + if (c == '+') { + *ret = ' '; + continue; + } + /* + * Cannot use strtol here because next char + * after %xx may be a digit. + */ + if (c == '%' && isxdigit((unsigned char)str[i + 1]) && + isxdigit((unsigned char)str[i + 2])) { + *ret = hextochar(&str[i + 1]); + i += 2; + continue; + } + *ret = c; + } + *ret = '\0'; + return ret - reallen; +} + +static char * +recode_credentials(const char *userinfo) +{ + char *ui, *creds; + size_t ulen; + + /* url-decode the user and pass */ + ui = url_decode(userinfo); + + ulen = strlen(ui); + if (base64_encode(ui, ulen, &creds) == -1) + errx(1, "error in base64 encoding"); + free(ui); + return (creds); +} + +/* + * Parse a proxy URI and split it up into host, port and userinfo. + */ +static void +proxy_parse_uri(char *uri) +{ + char *host, *port = NULL, *cred, *cookie = NULL; + + if (uri == NULL) + return; + + if (strncasecmp(uri, "http://", 7) != 0) + errx(1, "%s: http_proxy not using http schema", http_info(uri)); + + host = uri + 7; + if ((host = strndup(host, strcspn(host, "/"))) == NULL) + err(1, NULL); + + cred = host; + host = strchr(cred, '@'); + if (host != NULL) + *host++ = '\0'; + else { + host = cred; + cred = NULL; + } + + if (*host == '[') { + char *hosttail; + + if ((hosttail = strrchr(host, ']')) == NULL) + errx(1, "%s: unmatched opening bracket", + http_info(uri)); + if (hosttail[1] == '\0' || hosttail[1] == ':') + host++; + if (hosttail[1] == ':') + port = hosttail + 2; + *hosttail = '\0'; + } else { + if ((port = strrchr(host, ':')) != NULL) + *port++ = '\0'; + } + + if (port == NULL) + port = "443"; + + if (cred != NULL) { + if (strchr(cred, ':') == NULL) + errx(1, "%s: malformed proxy url", http_info(uri)); + cred = recode_credentials(cred); + if (asprintf(&cookie, "Proxy-Authorization: Basic %s\r\n", + cred) == -1) + err(1, NULL); + free(cred); + } else + if ((cookie = strdup("")) == NULL) + err(1, NULL); + + proxy.proxyhost = host; + proxy.proxyport = port; + proxy.proxyauth = cookie; +} + /* * Parse a URI and split it up into host, port and path. * Does some basic URI validation. Both host and port need to be freed @@ -301,8 +442,13 @@ http_parse_uri(char *uri, char **ohost, char **oport, char **opath) warnx("%s: preposterous host length", http_info(uri)); return -1; } + + if (memchr(host, '@', path - host) != NULL) { + warnx("%s: URI with userinfo not supported", http_info(uri)); + return -1; + } + if (*host == '[') { - char *scope; if ((hosttail = memrchr(host, ']', path - host)) == NULL) { warnx("%s: unmatched opening bracket", http_info(uri)); return -1; @@ -311,8 +457,6 @@ http_parse_uri(char *uri, char **ohost, char **oport, char **opath) host++; if (hosttail[1] == ':') port = hosttail + 2; - if ((scope = memchr(host, '%', hosttail - host)) != NULL) - hosttail = scope; } else { if ((hosttail = memrchr(host, ':', path - host)) != NULL) port = hosttail + 1; @@ -320,11 +464,6 @@ http_parse_uri(char *uri, char **ohost, char **oport, char **opath) hosttail = path; } - if (memchr(host, '@', hosttail - host) != NULL) { - warnx("%s: URI with userinfo not supported", http_info(uri)); - return -1; - } - if ((host = strndup(host, hosttail - host)) == NULL) err(1, NULL); if (port != NULL) { @@ -519,12 +658,19 @@ http_new(struct http_request *req) LIST_INSERT_HEAD(&active, conn, entry); http_conn_count++; - /* TODO proxy support (overload of host and port) */ - - if (http_resolv(&conn->res0, conn->host, conn->port) == -1) { - http_req_fail(req->id); - http_free(conn); - return; + if (proxy.proxyhost == NULL) { + if (http_resolv(&conn->res0, proxy.proxyhost, + proxy.proxyport) == -1) { + http_req_fail(req->id); + http_free(conn); + return; + } + } else { + if (http_resolv(&conn->res0, conn->host, conn->port) == -1) { + http_req_fail(req->id); + http_free(conn); + return; + } } /* connect and start request */ @@ -641,7 +787,7 @@ http_do(struct http_connection *conn, enum res (*f)(struct http_connection *)) } /* - * Connection successfully establish, initiate TLS handshake. + * Connection successfully establish, initiate TLS handshake or proxy request. */ static enum res http_connect_done(struct http_connection *conn) @@ -650,12 +796,8 @@ http_connect_done(struct http_connection *conn) conn->res0 = NULL; conn->res = NULL; -#if 0 - /* TODO proxy connect */ - if (proxyenv) - proxy_connect(conn->fd, sslhost, proxy_credentials); */ -#endif - + if (proxy.proxyhost == NULL) + return proxy_connect(conn); return http_tls_connect(conn); } @@ -798,10 +940,42 @@ http_tls_handshake(struct http_connection *conn) return WANT_POLLOUT; } - /* ready to send request */ return http_request(conn); } +static enum res +proxy_connect(struct http_connection *conn) +{ + char *host; + int r; + + assert(conn->state == STATE_CONNECT); + conn->state = STATE_PROXY_REQUEST; + + /* Construct the Host header from host and port info */ + if (strchr(conn->host, ':')) { + if (asprintf(&host, "[%s]:%s", conn->host, conn->port) == -1) + err(1, NULL); + + } else { + if (asprintf(&host, "%s:%s", conn->host, conn->port) == -1) + err(1, NULL); + } + + free(conn->buf); + conn->bufpos = 0; + /* XXX handle auth */ + if ((r = asprintf(&conn->buf, "CONNECT %s HTTP/1.1\r\n" + "User-Agent: " HTTP_USER_AGENT "\r\n%s\r\n", host, + proxy.proxyauth)) == -1) + err(1, NULL); + conn->bufsz = r; + + free(host); + + return proxy_write(conn); +} + /* * Build the HTTP request and send it out. */ @@ -1134,7 +1308,7 @@ read_more: s = tls_read(conn->tls, conn->buf + conn->bufpos, conn->bufsz - conn->bufpos); if (s == -1) { - warn("%s: TLS read: %s", http_info(conn->host), + warnx("%s: TLS read: %s", http_info(conn->host), tls_error(conn->tls)); return http_failed(conn); } else if (s == TLS_WANT_POLLIN) { @@ -1154,10 +1328,37 @@ read_more: again: switch (conn->state) { + case STATE_PROXY_STATUS: + buf = http_get_line(conn); + if (buf == NULL) + goto read_more; + if (http_parse_status(conn, buf) == -1) { + free(buf); + return http_failed(conn); + } + free(buf); + conn->state = STATE_PROXY_RESPONSE; + goto again; + case STATE_PROXY_RESPONSE: + while (1) { + buf = http_get_line(conn); + if (buf == NULL) + goto read_more; + /* empty line, end of header */ + if (*buf == '\0') + break; + } + /* proxy is ready to take connection */ + if (conn->status == 200) { + conn->state = STATE_CONNECT; + return http_tls_connect(conn); + } + return http_failed(conn); case STATE_RESPONSE_STATUS: buf = http_get_line(conn); if (buf == NULL) goto read_more; + if (http_parse_status(conn, buf) == -1) { free(buf); return http_failed(conn); @@ -1311,6 +1512,97 @@ http_write(struct http_connection *conn) return http_read(conn); } +static enum res +proxy_read(struct http_connection *conn) +{ + ssize_t s; + char *buf; + int done; + + s = read(conn->fd, conn->buf + conn->bufpos, + conn->bufsz - conn->bufpos); + if (s == -1) { + warn("%s: read", http_info(conn->host)); + return http_failed(conn); + } + + if (s == 0) { + if (conn->req) + warnx("%s: short read, connection closed", + http_info(conn->host)); + return http_failed(conn); + } + + conn->bufpos += s; + +again: + switch (conn->state) { + case STATE_PROXY_STATUS: + buf = http_get_line(conn); + if (buf == NULL) + return WANT_POLLIN; + if (http_parse_status(conn, buf) == -1) { + free(buf); + return http_failed(conn); + } + free(buf); + conn->state = STATE_PROXY_RESPONSE; + goto again; + case STATE_PROXY_RESPONSE: + done = 0; + while (!done) { + buf = http_get_line(conn); + if (buf == NULL) + return WANT_POLLIN; + /* empty line, end of header */ + if (*buf == '\0') + done = 1; + free(buf); + } + /* proxy is ready, connect to remote */ + if (conn->status == 200) { + conn->state = STATE_CONNECT; + return http_tls_connect(conn); + } + return http_failed(conn); + default: + errx(1, "unexpected http state"); + } +} + +/* + * Send out the proxy request. When done, replace buffer with the read buffer. + */ +static enum res +proxy_write(struct http_connection *conn) +{ + ssize_t s; + + assert(conn->state == STATE_PROXY_REQUEST); + + s = write(conn->fd, conn->buf + conn->bufpos, + conn->bufsz - conn->bufpos); + if (s == -1) { + warn("%s: write", http_info(conn->host)); + return http_failed(conn); + } + conn->bufpos += s; + if (conn->bufpos < conn->bufsz) + return WANT_POLLOUT; + + /* done writing, first thing we need the status */ + conn->state = STATE_PROXY_STATUS; + + /* free write buffer and allocate the read buffer */ + free(conn->buf); + conn->bufpos = 0; + conn->bufsz = HTTP_BUF_SIZE; + if ((conn->buf = malloc(conn->bufsz)) == NULL) + err(1, NULL); + + return WANT_POLLIN; +} + /* * Properly shutdown the TLS session else move connection into free state. */ @@ -1391,6 +1683,8 @@ http_handle(struct http_connection *conn) { assert (conn->pfd != NULL && conn->pfd->revents != 0); + conn->io_time = 0; + switch (conn->state) { case STATE_CONNECT: return http_finish_connect(conn); @@ -1398,6 +1692,11 @@ http_handle(struct http_connection *conn) return http_tls_handshake(conn); case STATE_REQUEST: return http_write(conn); + case STATE_PROXY_REQUEST: + return proxy_write(conn); + case STATE_PROXY_STATUS: + case STATE_PROXY_RESPONSE: + return proxy_read(conn); case STATE_RESPONSE_STATUS: case STATE_RESPONSE_HEADER: case STATE_RESPONSE_DATA: @@ -1423,6 +1722,8 @@ http_handle(struct http_connection *conn) static void http_setup(void) { + char *httpproxy; + tls_config = tls_config_new(); if (tls_config == NULL) errx(1, "tls config failed"); @@ -1444,7 +1745,10 @@ http_setup(void) err(1, "tls_load_file: %s", tls_default_ca_cert_file()); tls_config_set_ca_mem(tls_config, tls_ca_mem, tls_ca_size); - /* TODO initalize proxy settings */ + if ((httpproxy = getenv("http_proxy")) != NULL && *httpproxy == '\0') + httpproxy = NULL; + + proxy_parse_uri(httpproxy); } void @@ -1490,6 +1794,17 @@ proc_http(char *bind_addr, int fd) timeout = INFTIM; now = getmonotime(); LIST_FOREACH(conn, &active, entry) { + if (conn->io_time == 0) + conn->io_time = now + HTTP_IO_TIMEOUT; + + if (conn->io_time <= now) + timeout = 0; + else { + int diff = conn->io_time - now; + diff *= 1000; + if (timeout == INFTIM || diff < timeout) + timeout = diff; + } if (conn->state == STATE_WRITE_DATA) pfds[i].fd = conn->req->outfd; else @@ -1562,6 +1877,11 @@ proc_http(char *bind_addr, int fd) /* check if event is ready */ if (conn->pfd != NULL && conn->pfd->revents != 0) http_do(conn, http_handle); + else if (conn->io_time <= now) { + warnx("%s: timeout, connection closed", + http_info(conn->host)); + http_do(conn, http_failed); + } if (conn->state == STATE_FREE) http_free(conn); -- 2.20.1