mirror of
https://git.hardenedbsd.org/hardenedbsd/HardenedBSD.git
synced 2024-11-30 05:01:39 +01:00
fb3dc55a5c
Signed-off-by: rilysh <nightquick@proton.me> Reviewed by: imp Pull Request: https://github.com/freebsd/freebsd-src/pull/959
1772 lines
41 KiB
C
1772 lines
41 KiB
C
/*-
|
|
* SPDX-License-Identifier: BSD-3-Clause
|
|
*
|
|
* Copyright (c) 1998-2016 Dag-Erling Smørgrav
|
|
* Copyright (c) 2013 Michael Gmelin <freebsd@grem.de>
|
|
* All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer
|
|
* in this position and unchanged.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
* 3. The name of the author may not be used to endorse or promote products
|
|
* derived from this software without specific prior written permission
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
|
|
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
|
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
|
* IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
|
|
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
|
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
|
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
#include <sys/param.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/time.h>
|
|
#include <sys/uio.h>
|
|
|
|
#include <netinet/in.h>
|
|
|
|
#include <ctype.h>
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <inttypes.h>
|
|
#include <netdb.h>
|
|
#include <paths.h>
|
|
#include <poll.h>
|
|
#include <pwd.h>
|
|
#include <stdarg.h>
|
|
#include <stdlib.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
|
|
#ifdef WITH_SSL
|
|
#include <openssl/x509v3.h>
|
|
#endif
|
|
|
|
#include "fetch.h"
|
|
#include "common.h"
|
|
|
|
|
|
/*** Local data **************************************************************/
|
|
|
|
/*
|
|
* Error messages for resolver errors
|
|
*/
|
|
static struct fetcherr netdb_errlist[] = {
|
|
#ifdef EAI_ADDRFAMILY
|
|
{ EAI_ADDRFAMILY, FETCH_RESOLV, "Address family for host not supported" },
|
|
#endif
|
|
#ifdef EAI_NODATA
|
|
{ EAI_NODATA, FETCH_RESOLV, "No address for host" },
|
|
#endif
|
|
{ EAI_AGAIN, FETCH_TEMP, "Transient resolver failure" },
|
|
{ EAI_FAIL, FETCH_RESOLV, "Non-recoverable resolver failure" },
|
|
{ EAI_NONAME, FETCH_RESOLV, "Host does not resolve" },
|
|
{ -1, FETCH_UNKNOWN, "Unknown resolver error" }
|
|
};
|
|
|
|
/*
|
|
* SOCKS5 error enumerations
|
|
*/
|
|
enum SOCKS5_ERR {
|
|
/* Protocol errors */
|
|
SOCKS5_ERR_SELECTION,
|
|
SOCKS5_ERR_READ_METHOD,
|
|
SOCKS5_ERR_VER5_ONLY,
|
|
SOCKS5_ERR_NOMETHODS,
|
|
SOCKS5_ERR_NOTIMPLEMENTED,
|
|
SOCKS5_ERR_HOSTNAME_SIZE,
|
|
SOCKS5_ERR_REQUEST,
|
|
SOCKS5_ERR_REPLY,
|
|
SOCKS5_ERR_NON_VER5_RESP,
|
|
SOCKS5_ERR_GENERAL,
|
|
SOCKS5_ERR_NOT_ALLOWED,
|
|
SOCKS5_ERR_NET_UNREACHABLE,
|
|
SOCKS5_ERR_HOST_UNREACHABLE,
|
|
SOCKS5_ERR_CONN_REFUSED,
|
|
SOCKS5_ERR_TTL_EXPIRED,
|
|
SOCKS5_ERR_COM_UNSUPPORTED,
|
|
SOCKS5_ERR_ADDR_UNSUPPORTED,
|
|
SOCKS5_ERR_UNSPECIFIED,
|
|
/* Configuration errors */
|
|
SOCKS5_ERR_BAD_HOST,
|
|
SOCKS5_ERR_BAD_PROXY_FORMAT,
|
|
SOCKS5_ERR_BAD_PORT
|
|
};
|
|
|
|
/*
|
|
* Error messages for SOCKS5 errors
|
|
*/
|
|
static struct fetcherr socks5_errlist[] = {
|
|
/* SOCKS5 protocol errors */
|
|
{ SOCKS5_ERR_SELECTION, FETCH_ABORT, "SOCKS5: Failed to send selection method" },
|
|
{ SOCKS5_ERR_READ_METHOD, FETCH_ABORT, "SOCKS5: Failed to read method" },
|
|
{ SOCKS5_ERR_VER5_ONLY, FETCH_PROTO, "SOCKS5: Only version 5 is implemented" },
|
|
{ SOCKS5_ERR_NOMETHODS, FETCH_PROTO, "SOCKS5: No acceptable methods" },
|
|
{ SOCKS5_ERR_NOTIMPLEMENTED, FETCH_PROTO, "SOCKS5: Method currently not implemented" },
|
|
{ SOCKS5_ERR_HOSTNAME_SIZE, FETCH_PROTO, "SOCKS5: Hostname size is above 256 bytes" },
|
|
{ SOCKS5_ERR_REQUEST, FETCH_PROTO, "SOCKS5: Failed to request" },
|
|
{ SOCKS5_ERR_REPLY, FETCH_PROTO, "SOCKS5: Failed to receive reply" },
|
|
{ SOCKS5_ERR_NON_VER5_RESP, FETCH_PROTO, "SOCKS5: Server responded with a non-version 5 response" },
|
|
{ SOCKS5_ERR_GENERAL, FETCH_ABORT, "SOCKS5: General server failure" },
|
|
{ SOCKS5_ERR_NOT_ALLOWED, FETCH_AUTH, "SOCKS5: Connection not allowed by ruleset" },
|
|
{ SOCKS5_ERR_NET_UNREACHABLE, FETCH_NETWORK, "SOCKS5: Network unreachable" },
|
|
{ SOCKS5_ERR_HOST_UNREACHABLE, FETCH_ABORT, "SOCKS5: Host unreachable" },
|
|
{ SOCKS5_ERR_CONN_REFUSED, FETCH_ABORT, "SOCKS5: Connection refused" },
|
|
{ SOCKS5_ERR_TTL_EXPIRED, FETCH_TIMEOUT, "SOCKS5: TTL expired" },
|
|
{ SOCKS5_ERR_COM_UNSUPPORTED, FETCH_PROTO, "SOCKS5: Command not supported" },
|
|
{ SOCKS5_ERR_ADDR_UNSUPPORTED, FETCH_ABORT, "SOCKS5: Address type not supported" },
|
|
{ SOCKS5_ERR_UNSPECIFIED, FETCH_UNKNOWN, "SOCKS5: Unspecified error" },
|
|
/* Configuration error */
|
|
{ SOCKS5_ERR_BAD_HOST, FETCH_ABORT, "SOCKS5: Bad proxy host" },
|
|
{ SOCKS5_ERR_BAD_PROXY_FORMAT, FETCH_ABORT, "SOCKS5: Bad proxy format" },
|
|
{ SOCKS5_ERR_BAD_PORT, FETCH_ABORT, "SOCKS5: Bad port" }
|
|
};
|
|
|
|
/* End-of-Line */
|
|
static const char ENDL[2] = "\r\n";
|
|
|
|
|
|
/*** Error-reporting functions ***********************************************/
|
|
|
|
/*
|
|
* Map error code to string
|
|
*/
|
|
static struct fetcherr *
|
|
fetch_finderr(struct fetcherr *p, int e)
|
|
{
|
|
while (p->num != -1 && p->num != e)
|
|
p++;
|
|
return (p);
|
|
}
|
|
|
|
/*
|
|
* Set error code
|
|
*/
|
|
void
|
|
fetch_seterr(struct fetcherr *p, int e)
|
|
{
|
|
p = fetch_finderr(p, e);
|
|
fetchLastErrCode = p->cat;
|
|
snprintf(fetchLastErrString, MAXERRSTRING, "%s", p->string);
|
|
}
|
|
|
|
/*
|
|
* Set error code according to errno
|
|
*/
|
|
void
|
|
fetch_syserr(void)
|
|
{
|
|
switch (errno) {
|
|
case 0:
|
|
fetchLastErrCode = FETCH_OK;
|
|
break;
|
|
case EPERM:
|
|
case EACCES:
|
|
case EROFS:
|
|
case EAUTH:
|
|
case ENEEDAUTH:
|
|
fetchLastErrCode = FETCH_AUTH;
|
|
break;
|
|
case ENOENT:
|
|
case EISDIR: /* XXX */
|
|
fetchLastErrCode = FETCH_UNAVAIL;
|
|
break;
|
|
case ENOMEM:
|
|
fetchLastErrCode = FETCH_MEMORY;
|
|
break;
|
|
case EBUSY:
|
|
case EAGAIN:
|
|
fetchLastErrCode = FETCH_TEMP;
|
|
break;
|
|
case EEXIST:
|
|
fetchLastErrCode = FETCH_EXISTS;
|
|
break;
|
|
case ENOSPC:
|
|
fetchLastErrCode = FETCH_FULL;
|
|
break;
|
|
case EADDRINUSE:
|
|
case EADDRNOTAVAIL:
|
|
case ENETDOWN:
|
|
case ENETUNREACH:
|
|
case ENETRESET:
|
|
case EHOSTUNREACH:
|
|
fetchLastErrCode = FETCH_NETWORK;
|
|
break;
|
|
case ECONNABORTED:
|
|
case ECONNRESET:
|
|
fetchLastErrCode = FETCH_ABORT;
|
|
break;
|
|
case ETIMEDOUT:
|
|
fetchLastErrCode = FETCH_TIMEOUT;
|
|
break;
|
|
case ECONNREFUSED:
|
|
case EHOSTDOWN:
|
|
fetchLastErrCode = FETCH_DOWN;
|
|
break;
|
|
default:
|
|
fetchLastErrCode = FETCH_UNKNOWN;
|
|
}
|
|
snprintf(fetchLastErrString, MAXERRSTRING, "%s", strerror(errno));
|
|
}
|
|
|
|
|
|
/*
|
|
* Emit status message
|
|
*/
|
|
void
|
|
fetch_info(const char *fmt, ...)
|
|
{
|
|
va_list ap;
|
|
|
|
va_start(ap, fmt);
|
|
vfprintf(stderr, fmt, ap);
|
|
va_end(ap);
|
|
fputc('\n', stderr);
|
|
}
|
|
|
|
|
|
/*** Network-related utility functions ***************************************/
|
|
|
|
/*
|
|
* Return the default port for a scheme
|
|
*/
|
|
int
|
|
fetch_default_port(const char *scheme)
|
|
{
|
|
struct servent *se;
|
|
|
|
if ((se = getservbyname(scheme, "tcp")) != NULL)
|
|
return (ntohs(se->s_port));
|
|
if (strcmp(scheme, SCHEME_FTP) == 0)
|
|
return (FTP_DEFAULT_PORT);
|
|
if (strcmp(scheme, SCHEME_HTTP) == 0)
|
|
return (HTTP_DEFAULT_PORT);
|
|
return (0);
|
|
}
|
|
|
|
/*
|
|
* Return the default proxy port for a scheme
|
|
*/
|
|
int
|
|
fetch_default_proxy_port(const char *scheme)
|
|
{
|
|
if (strcmp(scheme, SCHEME_FTP) == 0)
|
|
return (FTP_DEFAULT_PROXY_PORT);
|
|
if (strcmp(scheme, SCHEME_HTTP) == 0)
|
|
return (HTTP_DEFAULT_PROXY_PORT);
|
|
return (0);
|
|
}
|
|
|
|
|
|
/*
|
|
* Create a connection for an existing descriptor.
|
|
*/
|
|
conn_t *
|
|
fetch_reopen(int sd)
|
|
{
|
|
conn_t *conn;
|
|
int opt = 1;
|
|
|
|
/* allocate and fill connection structure */
|
|
if ((conn = calloc(1, sizeof(*conn))) == NULL)
|
|
return (NULL);
|
|
fcntl(sd, F_SETFD, FD_CLOEXEC);
|
|
setsockopt(sd, SOL_SOCKET, SO_NOSIGPIPE, &opt, sizeof opt);
|
|
conn->sd = sd;
|
|
++conn->ref;
|
|
return (conn);
|
|
}
|
|
|
|
|
|
/*
|
|
* Bump a connection's reference count.
|
|
*/
|
|
conn_t *
|
|
fetch_ref(conn_t *conn)
|
|
{
|
|
|
|
++conn->ref;
|
|
return (conn);
|
|
}
|
|
|
|
|
|
/*
|
|
* Resolve an address
|
|
*/
|
|
struct addrinfo *
|
|
fetch_resolve(const char *addr, int port, int af)
|
|
{
|
|
char hbuf[256], sbuf[8];
|
|
struct addrinfo hints, *res;
|
|
const char *hb, *he, *sep;
|
|
const char *host, *service;
|
|
int err, len;
|
|
|
|
/* first, check for a bracketed IPv6 address */
|
|
if (*addr == '[') {
|
|
hb = addr + 1;
|
|
if ((sep = strchr(hb, ']')) == NULL) {
|
|
errno = EINVAL;
|
|
goto syserr;
|
|
}
|
|
he = sep++;
|
|
} else {
|
|
hb = addr;
|
|
sep = strchrnul(hb, ':');
|
|
he = sep;
|
|
}
|
|
|
|
/* see if we need to copy the host name */
|
|
if (*he != '\0') {
|
|
len = snprintf(hbuf, sizeof(hbuf),
|
|
"%.*s", (int)(he - hb), hb);
|
|
if (len < 0)
|
|
goto syserr;
|
|
if (len >= (int)sizeof(hbuf)) {
|
|
errno = ENAMETOOLONG;
|
|
goto syserr;
|
|
}
|
|
host = hbuf;
|
|
} else {
|
|
host = hb;
|
|
}
|
|
|
|
/* was it followed by a service name? */
|
|
if (*sep == '\0' && port != 0) {
|
|
if (port < 1 || port > 65535) {
|
|
errno = EINVAL;
|
|
goto syserr;
|
|
}
|
|
if (snprintf(sbuf, sizeof(sbuf), "%d", port) < 0)
|
|
goto syserr;
|
|
service = sbuf;
|
|
} else if (*sep != '\0') {
|
|
service = sep + 1;
|
|
} else {
|
|
service = NULL;
|
|
}
|
|
|
|
/* resolve */
|
|
memset(&hints, 0, sizeof(hints));
|
|
hints.ai_family = af;
|
|
hints.ai_socktype = SOCK_STREAM;
|
|
hints.ai_flags = AI_ADDRCONFIG;
|
|
if ((err = getaddrinfo(host, service, &hints, &res)) != 0) {
|
|
netdb_seterr(err);
|
|
return (NULL);
|
|
}
|
|
return (res);
|
|
syserr:
|
|
fetch_syserr();
|
|
return (NULL);
|
|
}
|
|
|
|
|
|
/*
|
|
* Bind a socket to a specific local address
|
|
*/
|
|
int
|
|
fetch_bind(int sd, int af, const char *addr)
|
|
{
|
|
struct addrinfo *cliai, *ai;
|
|
int err;
|
|
|
|
if ((cliai = fetch_resolve(addr, 0, af)) == NULL)
|
|
return (-1);
|
|
for (ai = cliai; ai != NULL; ai = ai->ai_next)
|
|
if ((err = bind(sd, ai->ai_addr, ai->ai_addrlen)) == 0)
|
|
break;
|
|
if (err != 0)
|
|
fetch_syserr();
|
|
freeaddrinfo(cliai);
|
|
return (err == 0 ? 0 : -1);
|
|
}
|
|
|
|
|
|
/*
|
|
* SOCKS5 connection initiation, based on RFC 1928
|
|
* Default DNS resolution over SOCKS5
|
|
*/
|
|
int
|
|
fetch_socks5_init(conn_t *conn, const char *host, int port, int verbose)
|
|
{
|
|
/*
|
|
* Size is based on largest packet prefix (4 bytes) +
|
|
* Largest FQDN (256) + one byte size (1) +
|
|
* Port (2)
|
|
*/
|
|
unsigned char buf[BUFF_SIZE];
|
|
unsigned char *ptr;
|
|
int ret = 1;
|
|
|
|
if (verbose)
|
|
fetch_info("Initializing SOCKS5 connection: %s:%d", host, port);
|
|
|
|
/* Connection initialization */
|
|
ptr = buf;
|
|
*ptr++ = SOCKS_VERSION_5;
|
|
*ptr++ = SOCKS_CONNECTION;
|
|
*ptr++ = SOCKS_RSV;
|
|
|
|
if (fetch_write(conn, buf, 3) != 3) {
|
|
ret = SOCKS5_ERR_SELECTION;
|
|
goto fail;
|
|
}
|
|
|
|
/* Verify response from SOCKS5 server */
|
|
if (fetch_read(conn, buf, 2) != 2) {
|
|
ret = SOCKS5_ERR_READ_METHOD;
|
|
goto fail;
|
|
}
|
|
|
|
ptr = buf;
|
|
if (ptr[0] != SOCKS_VERSION_5) {
|
|
ret = SOCKS5_ERR_VER5_ONLY;
|
|
goto fail;
|
|
}
|
|
if (ptr[1] == SOCKS_NOMETHODS) {
|
|
ret = SOCKS5_ERR_NOMETHODS;
|
|
goto fail;
|
|
}
|
|
else if (ptr[1] != SOCKS5_NOTIMPLEMENTED) {
|
|
ret = SOCKS5_ERR_NOTIMPLEMENTED;
|
|
goto fail;
|
|
}
|
|
|
|
/* Send Request */
|
|
*ptr++ = SOCKS_VERSION_5;
|
|
*ptr++ = SOCKS_CONNECTION;
|
|
*ptr++ = SOCKS_RSV;
|
|
/* Encode all targets as a hostname to avoid DNS leaks */
|
|
*ptr++ = SOCKS_ATYP_DOMAINNAME;
|
|
if (strlen(host) > FQDN_SIZE) {
|
|
ret = SOCKS5_ERR_HOSTNAME_SIZE;
|
|
goto fail;
|
|
}
|
|
*ptr++ = strlen(host);
|
|
memcpy(ptr, host, strlen(host));
|
|
ptr = ptr + strlen(host);
|
|
|
|
port = htons(port);
|
|
*ptr++ = port & 0x00ff;
|
|
*ptr++ = (port & 0xff00) >> 8;
|
|
|
|
if (fetch_write(conn, buf, ptr - buf) != ptr - buf) {
|
|
ret = SOCKS5_ERR_REQUEST;
|
|
goto fail;
|
|
}
|
|
|
|
/* BND.ADDR is variable length, read the largest on non-blocking socket */
|
|
if (!fetch_read(conn, buf, BUFF_SIZE)) {
|
|
ret = SOCKS5_ERR_REPLY;
|
|
goto fail;
|
|
}
|
|
|
|
ptr = buf;
|
|
if (*ptr++ != SOCKS_VERSION_5) {
|
|
ret = SOCKS5_ERR_NON_VER5_RESP;
|
|
goto fail;
|
|
}
|
|
|
|
switch(*ptr++) {
|
|
case SOCKS_SUCCESS:
|
|
break;
|
|
case SOCKS_GENERAL_FAILURE:
|
|
ret = SOCKS5_ERR_GENERAL;
|
|
goto fail;
|
|
case SOCKS_CONNECTION_NOT_ALLOWED:
|
|
ret = SOCKS5_ERR_NOT_ALLOWED;
|
|
goto fail;
|
|
case SOCKS_NETWORK_UNREACHABLE:
|
|
ret = SOCKS5_ERR_NET_UNREACHABLE;
|
|
goto fail;
|
|
case SOCKS_HOST_UNREACHABLE:
|
|
ret = SOCKS5_ERR_HOST_UNREACHABLE;
|
|
goto fail;
|
|
case SOCKS_CONNECTION_REFUSED:
|
|
ret = SOCKS5_ERR_CONN_REFUSED;
|
|
goto fail;
|
|
case SOCKS_TTL_EXPIRED:
|
|
ret = SOCKS5_ERR_TTL_EXPIRED;
|
|
goto fail;
|
|
case SOCKS_COMMAND_NOT_SUPPORTED:
|
|
ret = SOCKS5_ERR_COM_UNSUPPORTED;
|
|
goto fail;
|
|
case SOCKS_ADDRESS_NOT_SUPPORTED:
|
|
ret = SOCKS5_ERR_ADDR_UNSUPPORTED;
|
|
goto fail;
|
|
default:
|
|
ret = SOCKS5_ERR_UNSPECIFIED;
|
|
goto fail;
|
|
}
|
|
|
|
return (ret);
|
|
|
|
fail:
|
|
socks5_seterr(ret);
|
|
return (0);
|
|
}
|
|
|
|
/*
|
|
* Perform SOCKS5 initialization
|
|
*/
|
|
int
|
|
fetch_socks5_getenv(char **host, int *port)
|
|
{
|
|
char *socks5env, *endptr, *ext;
|
|
const char *portDelim;
|
|
size_t slen;
|
|
|
|
portDelim = ":";
|
|
if ((socks5env = getenv("SOCKS5_PROXY")) == NULL || *socks5env == '\0') {
|
|
*host = NULL;
|
|
*port = -1;
|
|
return (-1);
|
|
}
|
|
|
|
/*
|
|
* IPv6 addresses begin and end in brackets. Set the port delimiter
|
|
* accordingly and search for it so we can do appropriate validation.
|
|
*/
|
|
if (socks5env[0] == '[')
|
|
portDelim = "]:";
|
|
|
|
slen = strlen(socks5env);
|
|
ext = strstr(socks5env, portDelim);
|
|
if (socks5env[0] == '[') {
|
|
if (socks5env[slen - 1] == ']') {
|
|
*host = strndup(socks5env, slen);
|
|
} else if (ext != NULL) {
|
|
*host = strndup(socks5env, ext - socks5env + 1);
|
|
} else {
|
|
socks5_seterr(SOCKS5_ERR_BAD_PROXY_FORMAT);
|
|
return (0);
|
|
}
|
|
} else {
|
|
*host = strndup(socks5env, ext - socks5env);
|
|
}
|
|
|
|
if (*host == NULL) {
|
|
fprintf(stderr, "Failure to allocate memory, exiting.\n");
|
|
return (-1);
|
|
}
|
|
if (ext == NULL) {
|
|
*port = 1080; /* Default port as defined in RFC1928 */
|
|
} else {
|
|
ext += strlen(portDelim);
|
|
errno = 0;
|
|
*port = strtoimax(ext, (char **)&endptr, 10);
|
|
if (*endptr != '\0' || errno != 0 || *port < 0 ||
|
|
*port > 65535) {
|
|
free(*host);
|
|
*host = NULL;
|
|
socks5_seterr(SOCKS5_ERR_BAD_PORT);
|
|
return (0);
|
|
}
|
|
}
|
|
|
|
return (2);
|
|
}
|
|
|
|
|
|
/*
|
|
* Establish a TCP connection to the specified port on the specified host.
|
|
*/
|
|
conn_t *
|
|
fetch_connect(const char *host, int port, int af, int verbose)
|
|
{
|
|
struct addrinfo *cais = NULL, *sais = NULL, *cai, *sai;
|
|
const char *bindaddr;
|
|
conn_t *conn = NULL;
|
|
int err = 0, sd = -1;
|
|
char *sockshost;
|
|
int socksport;
|
|
|
|
DEBUGF("---> %s:%d\n", host, port);
|
|
|
|
/*
|
|
* Check if SOCKS5_PROXY env variable is set. fetch_socks5_getenv
|
|
* will either set sockshost = NULL or allocate memory in all cases.
|
|
*/
|
|
sockshost = NULL;
|
|
if (!fetch_socks5_getenv(&sockshost, &socksport))
|
|
goto fail;
|
|
|
|
/* Not using SOCKS5 proxy */
|
|
if (sockshost == NULL) {
|
|
/* resolve server address */
|
|
if (verbose)
|
|
fetch_info("resolving server address: %s:%d", host,
|
|
port);
|
|
if ((sais = fetch_resolve(host, port, af)) == NULL)
|
|
goto fail;
|
|
|
|
/* resolve client address */
|
|
bindaddr = getenv("FETCH_BIND_ADDRESS");
|
|
if (bindaddr != NULL && *bindaddr != '\0') {
|
|
if (verbose)
|
|
fetch_info("resolving client address: %s",
|
|
bindaddr);
|
|
if ((cais = fetch_resolve(bindaddr, 0, af)) == NULL)
|
|
goto fail;
|
|
}
|
|
} else {
|
|
/* resolve socks5 proxy address */
|
|
if (verbose)
|
|
fetch_info("resolving SOCKS5 server address: %s:%d",
|
|
sockshost, socksport);
|
|
if ((sais = fetch_resolve(sockshost, socksport, af)) == NULL) {
|
|
socks5_seterr(SOCKS5_ERR_BAD_HOST);
|
|
goto fail;
|
|
}
|
|
}
|
|
|
|
/* try each server address in turn */
|
|
for (err = 0, sai = sais; sai != NULL; sai = sai->ai_next) {
|
|
/* open socket */
|
|
if ((sd = socket(sai->ai_family, SOCK_STREAM, 0)) < 0)
|
|
goto syserr;
|
|
/* attempt to bind to client address */
|
|
for (err = 0, cai = cais; cai != NULL; cai = cai->ai_next) {
|
|
if (cai->ai_family != sai->ai_family)
|
|
continue;
|
|
if ((err = bind(sd, cai->ai_addr, cai->ai_addrlen)) == 0)
|
|
break;
|
|
}
|
|
if (err != 0) {
|
|
if (verbose)
|
|
fetch_info("failed to bind to %s", bindaddr);
|
|
goto syserr;
|
|
}
|
|
/* attempt to connect to server address */
|
|
if ((err = connect(sd, sai->ai_addr, sai->ai_addrlen)) == 0)
|
|
break;
|
|
/* clean up before next attempt */
|
|
close(sd);
|
|
sd = -1;
|
|
}
|
|
if (err != 0) {
|
|
if (verbose && sockshost == NULL) {
|
|
fetch_info("failed to connect to %s:%d", host, port);
|
|
goto syserr;
|
|
} else if (sockshost != NULL) {
|
|
if (verbose)
|
|
fetch_info(
|
|
"failed to connect to SOCKS5 server %s:%d",
|
|
sockshost, socksport);
|
|
socks5_seterr(SOCKS5_ERR_CONN_REFUSED);
|
|
goto fail;
|
|
}
|
|
goto syserr;
|
|
}
|
|
|
|
if ((conn = fetch_reopen(sd)) == NULL)
|
|
goto syserr;
|
|
|
|
if (sockshost)
|
|
if (!fetch_socks5_init(conn, host, port, verbose))
|
|
goto fail;
|
|
free(sockshost);
|
|
if (cais != NULL)
|
|
freeaddrinfo(cais);
|
|
if (sais != NULL)
|
|
freeaddrinfo(sais);
|
|
return (conn);
|
|
syserr:
|
|
fetch_syserr();
|
|
fail:
|
|
free(sockshost);
|
|
/* Fully close if it was opened; otherwise just don't leak the fd. */
|
|
if (conn != NULL)
|
|
fetch_close(conn);
|
|
else if (sd >= 0)
|
|
close(sd);
|
|
if (cais != NULL)
|
|
freeaddrinfo(cais);
|
|
if (sais != NULL)
|
|
freeaddrinfo(sais);
|
|
return (NULL);
|
|
}
|
|
|
|
#ifdef WITH_SSL
|
|
/*
|
|
* Convert characters A-Z to lowercase (intentionally avoid any locale
|
|
* specific conversions).
|
|
*/
|
|
static char
|
|
fetch_ssl_tolower(char in)
|
|
{
|
|
if (in >= 'A' && in <= 'Z')
|
|
return (in + 32);
|
|
else
|
|
return (in);
|
|
}
|
|
|
|
/*
|
|
* isalpha implementation that intentionally avoids any locale specific
|
|
* conversions.
|
|
*/
|
|
static int
|
|
fetch_ssl_isalpha(char in)
|
|
{
|
|
return ((in >= 'A' && in <= 'Z') || (in >= 'a' && in <= 'z'));
|
|
}
|
|
|
|
/*
|
|
* Check if passed hostnames a and b are equal.
|
|
*/
|
|
static int
|
|
fetch_ssl_hname_equal(const char *a, size_t alen, const char *b,
|
|
size_t blen)
|
|
{
|
|
size_t i;
|
|
|
|
if (alen != blen)
|
|
return (0);
|
|
for (i = 0; i < alen; ++i) {
|
|
if (fetch_ssl_tolower(a[i]) != fetch_ssl_tolower(b[i]))
|
|
return (0);
|
|
}
|
|
return (1);
|
|
}
|
|
|
|
/*
|
|
* Check if domain label is traditional, meaning that only A-Z, a-z, 0-9
|
|
* and '-' (hyphen) are allowed. Hyphens have to be surrounded by alpha-
|
|
* numeric characters. Double hyphens (like they're found in IDN a-labels
|
|
* 'xn--') are not allowed. Empty labels are invalid.
|
|
*/
|
|
static int
|
|
fetch_ssl_is_trad_domain_label(const char *l, size_t len, int wcok)
|
|
{
|
|
size_t i;
|
|
|
|
if (!len || l[0] == '-' || l[len-1] == '-')
|
|
return (0);
|
|
for (i = 0; i < len; ++i) {
|
|
if (!isdigit(l[i]) &&
|
|
!fetch_ssl_isalpha(l[i]) &&
|
|
!(l[i] == '*' && wcok) &&
|
|
!(l[i] == '-' && l[i - 1] != '-'))
|
|
return (0);
|
|
}
|
|
return (1);
|
|
}
|
|
|
|
/*
|
|
* Check if host name consists only of numbers. This might indicate an IP
|
|
* address, which is not a good idea for CN wildcard comparison.
|
|
*/
|
|
static int
|
|
fetch_ssl_hname_is_only_numbers(const char *hostname, size_t len)
|
|
{
|
|
size_t i;
|
|
|
|
for (i = 0; i < len; ++i) {
|
|
if (!((hostname[i] >= '0' && hostname[i] <= '9') ||
|
|
hostname[i] == '.'))
|
|
return (0);
|
|
}
|
|
return (1);
|
|
}
|
|
|
|
/*
|
|
* Check if the host name h passed matches the pattern passed in m which
|
|
* is usually part of subjectAltName or CN of a certificate presented to
|
|
* the client. This includes wildcard matching. The algorithm is based on
|
|
* RFC6125, sections 6.4.3 and 7.2, which clarifies RFC2818 and RFC3280.
|
|
*/
|
|
static int
|
|
fetch_ssl_hname_match(const char *h, size_t hlen, const char *m,
|
|
size_t mlen)
|
|
{
|
|
int delta, hdotidx, mdot1idx, wcidx;
|
|
const char *hdot, *mdot1, *mdot2;
|
|
const char *wc; /* wildcard */
|
|
|
|
if (!(h && *h && m && *m))
|
|
return (0);
|
|
if ((wc = strnstr(m, "*", mlen)) == NULL)
|
|
return (fetch_ssl_hname_equal(h, hlen, m, mlen));
|
|
wcidx = wc - m;
|
|
/* hostname should not be just dots and numbers */
|
|
if (fetch_ssl_hname_is_only_numbers(h, hlen))
|
|
return (0);
|
|
/* only one wildcard allowed in pattern */
|
|
if (strnstr(wc + 1, "*", mlen - wcidx - 1) != NULL)
|
|
return (0);
|
|
/*
|
|
* there must be at least two more domain labels and
|
|
* wildcard has to be in the leftmost label (RFC6125)
|
|
*/
|
|
mdot1 = strnstr(m, ".", mlen);
|
|
if (mdot1 == NULL || mdot1 < wc || (mlen - (mdot1 - m)) < 4)
|
|
return (0);
|
|
mdot1idx = mdot1 - m;
|
|
mdot2 = strnstr(mdot1 + 1, ".", mlen - mdot1idx - 1);
|
|
if (mdot2 == NULL || (mlen - (mdot2 - m)) < 2)
|
|
return (0);
|
|
/* hostname must contain a dot and not be the 1st char */
|
|
hdot = strnstr(h, ".", hlen);
|
|
if (hdot == NULL || hdot == h)
|
|
return (0);
|
|
hdotidx = hdot - h;
|
|
/*
|
|
* host part of hostname must be at least as long as
|
|
* pattern it's supposed to match
|
|
*/
|
|
if (hdotidx < mdot1idx)
|
|
return (0);
|
|
/*
|
|
* don't allow wildcards in non-traditional domain names
|
|
* (IDN, A-label, U-label...)
|
|
*/
|
|
if (!fetch_ssl_is_trad_domain_label(h, hdotidx, 0) ||
|
|
!fetch_ssl_is_trad_domain_label(m, mdot1idx, 1))
|
|
return (0);
|
|
/* match domain part (part after first dot) */
|
|
if (!fetch_ssl_hname_equal(hdot, hlen - hdotidx, mdot1,
|
|
mlen - mdot1idx))
|
|
return (0);
|
|
/* match part left of wildcard */
|
|
if (!fetch_ssl_hname_equal(h, wcidx, m, wcidx))
|
|
return (0);
|
|
/* match part right of wildcard */
|
|
delta = mdot1idx - wcidx - 1;
|
|
if (!fetch_ssl_hname_equal(hdot - delta, delta,
|
|
mdot1 - delta, delta))
|
|
return (0);
|
|
/* all tests succeeded, it's a match */
|
|
return (1);
|
|
}
|
|
|
|
/*
|
|
* Get numeric host address info - returns NULL if host was not an IP
|
|
* address. The caller is responsible for deallocation using
|
|
* freeaddrinfo(3).
|
|
*/
|
|
static struct addrinfo *
|
|
fetch_ssl_get_numeric_addrinfo(const char *hostname, size_t len)
|
|
{
|
|
struct addrinfo hints, *res;
|
|
char *host;
|
|
|
|
host = (char *)malloc(len + 1);
|
|
memcpy(host, hostname, len);
|
|
host[len] = '\0';
|
|
memset(&hints, 0, sizeof(hints));
|
|
hints.ai_family = PF_UNSPEC;
|
|
hints.ai_socktype = SOCK_STREAM;
|
|
hints.ai_protocol = 0;
|
|
hints.ai_flags = AI_NUMERICHOST;
|
|
/* port is not relevant for this purpose */
|
|
if (getaddrinfo(host, "443", &hints, &res) != 0)
|
|
res = NULL;
|
|
free(host);
|
|
return res;
|
|
}
|
|
|
|
/*
|
|
* Compare ip address in addrinfo with address passes.
|
|
*/
|
|
static int
|
|
fetch_ssl_ipaddr_match_bin(const struct addrinfo *lhost, const char *rhost,
|
|
size_t rhostlen)
|
|
{
|
|
const void *left;
|
|
|
|
if (lhost->ai_family == AF_INET && rhostlen == 4) {
|
|
left = (void *)&((struct sockaddr_in*)(void *)
|
|
lhost->ai_addr)->sin_addr.s_addr;
|
|
#ifdef INET6
|
|
} else if (lhost->ai_family == AF_INET6 && rhostlen == 16) {
|
|
left = (void *)&((struct sockaddr_in6 *)(void *)
|
|
lhost->ai_addr)->sin6_addr;
|
|
#endif
|
|
} else
|
|
return (0);
|
|
return (!memcmp(left, (const void *)rhost, rhostlen) ? 1 : 0);
|
|
}
|
|
|
|
/*
|
|
* Compare ip address in addrinfo with host passed. If host is not an IP
|
|
* address, comparison will fail.
|
|
*/
|
|
static int
|
|
fetch_ssl_ipaddr_match(const struct addrinfo *laddr, const char *r,
|
|
size_t rlen)
|
|
{
|
|
struct addrinfo *raddr;
|
|
int ret;
|
|
char *rip;
|
|
|
|
ret = 0;
|
|
if ((raddr = fetch_ssl_get_numeric_addrinfo(r, rlen)) == NULL)
|
|
return 0; /* not a numeric host */
|
|
|
|
if (laddr->ai_family == raddr->ai_family) {
|
|
if (laddr->ai_family == AF_INET) {
|
|
rip = (char *)&((struct sockaddr_in *)(void *)
|
|
raddr->ai_addr)->sin_addr.s_addr;
|
|
ret = fetch_ssl_ipaddr_match_bin(laddr, rip, 4);
|
|
#ifdef INET6
|
|
} else if (laddr->ai_family == AF_INET6) {
|
|
rip = (char *)&((struct sockaddr_in6 *)(void *)
|
|
raddr->ai_addr)->sin6_addr;
|
|
ret = fetch_ssl_ipaddr_match_bin(laddr, rip, 16);
|
|
#endif
|
|
}
|
|
|
|
}
|
|
freeaddrinfo(raddr);
|
|
return (ret);
|
|
}
|
|
|
|
/*
|
|
* Verify server certificate by subjectAltName.
|
|
*/
|
|
static int
|
|
fetch_ssl_verify_altname(STACK_OF(GENERAL_NAME) *altnames,
|
|
const char *host, struct addrinfo *ip)
|
|
{
|
|
const GENERAL_NAME *name;
|
|
size_t nslen;
|
|
int i;
|
|
const char *ns;
|
|
|
|
for (i = 0; i < sk_GENERAL_NAME_num(altnames); ++i) {
|
|
name = sk_GENERAL_NAME_value(altnames, i);
|
|
ns = (const char *)ASN1_STRING_get0_data(name->d.ia5);
|
|
nslen = (size_t)ASN1_STRING_length(name->d.ia5);
|
|
|
|
if (name->type == GEN_DNS && ip == NULL &&
|
|
fetch_ssl_hname_match(host, strlen(host), ns, nslen))
|
|
return (1);
|
|
else if (name->type == GEN_IPADD && ip != NULL &&
|
|
fetch_ssl_ipaddr_match_bin(ip, ns, nslen))
|
|
return (1);
|
|
}
|
|
return (0);
|
|
}
|
|
|
|
/*
|
|
* Verify server certificate by CN.
|
|
*/
|
|
static int
|
|
fetch_ssl_verify_cn(X509_NAME *subject, const char *host,
|
|
struct addrinfo *ip)
|
|
{
|
|
ASN1_STRING *namedata;
|
|
X509_NAME_ENTRY *nameentry;
|
|
int cnlen, lastpos, loc, ret;
|
|
unsigned char *cn;
|
|
|
|
ret = 0;
|
|
lastpos = -1;
|
|
loc = -1;
|
|
cn = NULL;
|
|
/* get most specific CN (last entry in list) and compare */
|
|
while ((lastpos = X509_NAME_get_index_by_NID(subject,
|
|
NID_commonName, lastpos)) != -1)
|
|
loc = lastpos;
|
|
|
|
if (loc > -1) {
|
|
nameentry = X509_NAME_get_entry(subject, loc);
|
|
namedata = X509_NAME_ENTRY_get_data(nameentry);
|
|
cnlen = ASN1_STRING_to_UTF8(&cn, namedata);
|
|
if (ip == NULL &&
|
|
fetch_ssl_hname_match(host, strlen(host), cn, cnlen))
|
|
ret = 1;
|
|
else if (ip != NULL && fetch_ssl_ipaddr_match(ip, cn, cnlen))
|
|
ret = 1;
|
|
OPENSSL_free(cn);
|
|
}
|
|
return (ret);
|
|
}
|
|
|
|
/*
|
|
* Verify that server certificate subjectAltName/CN matches
|
|
* hostname. First check, if there are alternative subject names. If yes,
|
|
* those have to match. Only if those don't exist it falls back to
|
|
* checking the subject's CN.
|
|
*/
|
|
static int
|
|
fetch_ssl_verify_hname(X509 *cert, const char *host)
|
|
{
|
|
struct addrinfo *ip;
|
|
STACK_OF(GENERAL_NAME) *altnames;
|
|
X509_NAME *subject;
|
|
int ret;
|
|
|
|
ret = 0;
|
|
ip = fetch_ssl_get_numeric_addrinfo(host, strlen(host));
|
|
altnames = X509_get_ext_d2i(cert, NID_subject_alt_name,
|
|
NULL, NULL);
|
|
|
|
if (altnames != NULL) {
|
|
ret = fetch_ssl_verify_altname(altnames, host, ip);
|
|
} else {
|
|
subject = X509_get_subject_name(cert);
|
|
if (subject != NULL)
|
|
ret = fetch_ssl_verify_cn(subject, host, ip);
|
|
}
|
|
|
|
if (ip != NULL)
|
|
freeaddrinfo(ip);
|
|
if (altnames != NULL)
|
|
GENERAL_NAMES_free(altnames);
|
|
return (ret);
|
|
}
|
|
|
|
/*
|
|
* Configure transport security layer based on environment.
|
|
*/
|
|
static void
|
|
fetch_ssl_setup_transport_layer(SSL_CTX *ctx, int verbose)
|
|
{
|
|
long ssl_ctx_options;
|
|
|
|
ssl_ctx_options = SSL_OP_ALL | SSL_OP_NO_SSLv3 | SSL_OP_NO_TICKET;
|
|
if (getenv("SSL_NO_TLS1") != NULL)
|
|
ssl_ctx_options |= SSL_OP_NO_TLSv1;
|
|
if (getenv("SSL_NO_TLS1_1") != NULL)
|
|
ssl_ctx_options |= SSL_OP_NO_TLSv1_1;
|
|
if (getenv("SSL_NO_TLS1_2") != NULL)
|
|
ssl_ctx_options |= SSL_OP_NO_TLSv1_2;
|
|
if (verbose)
|
|
fetch_info("SSL options: %lx", ssl_ctx_options);
|
|
SSL_CTX_set_options(ctx, ssl_ctx_options);
|
|
}
|
|
|
|
|
|
/*
|
|
* Configure peer verification based on environment.
|
|
*/
|
|
static int
|
|
fetch_ssl_setup_peer_verification(SSL_CTX *ctx, int verbose)
|
|
{
|
|
X509_LOOKUP *crl_lookup;
|
|
X509_STORE *crl_store;
|
|
const char *ca_cert_file, *ca_cert_path, *crl_file;
|
|
|
|
if (getenv("SSL_NO_VERIFY_PEER") == NULL) {
|
|
ca_cert_file = getenv("SSL_CA_CERT_FILE");
|
|
ca_cert_path = getenv("SSL_CA_CERT_PATH");
|
|
if (verbose) {
|
|
fetch_info("Peer verification enabled");
|
|
if (ca_cert_file != NULL)
|
|
fetch_info("Using CA cert file: %s",
|
|
ca_cert_file);
|
|
if (ca_cert_path != NULL)
|
|
fetch_info("Using CA cert path: %s",
|
|
ca_cert_path);
|
|
if (ca_cert_file == NULL && ca_cert_path == NULL)
|
|
fetch_info("Using OpenSSL default "
|
|
"CA cert file and path");
|
|
}
|
|
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER,
|
|
fetch_ssl_cb_verify_crt);
|
|
if (ca_cert_file != NULL || ca_cert_path != NULL)
|
|
SSL_CTX_load_verify_locations(ctx, ca_cert_file,
|
|
ca_cert_path);
|
|
else
|
|
SSL_CTX_set_default_verify_paths(ctx);
|
|
if ((crl_file = getenv("SSL_CRL_FILE")) != NULL) {
|
|
if (verbose)
|
|
fetch_info("Using CRL file: %s", crl_file);
|
|
crl_store = SSL_CTX_get_cert_store(ctx);
|
|
crl_lookup = X509_STORE_add_lookup(crl_store,
|
|
X509_LOOKUP_file());
|
|
if (crl_lookup == NULL ||
|
|
!X509_load_crl_file(crl_lookup, crl_file,
|
|
X509_FILETYPE_PEM)) {
|
|
fprintf(stderr,
|
|
"Could not load CRL file %s\n",
|
|
crl_file);
|
|
return (0);
|
|
}
|
|
X509_STORE_set_flags(crl_store,
|
|
X509_V_FLAG_CRL_CHECK |
|
|
X509_V_FLAG_CRL_CHECK_ALL);
|
|
}
|
|
}
|
|
return (1);
|
|
}
|
|
|
|
/*
|
|
* Configure client certificate based on environment.
|
|
*/
|
|
static int
|
|
fetch_ssl_setup_client_certificate(SSL_CTX *ctx, int verbose)
|
|
{
|
|
const char *client_cert_file, *client_key_file;
|
|
|
|
if ((client_cert_file = getenv("SSL_CLIENT_CERT_FILE")) != NULL) {
|
|
client_key_file = getenv("SSL_CLIENT_KEY_FILE") != NULL ?
|
|
getenv("SSL_CLIENT_KEY_FILE") : client_cert_file;
|
|
if (verbose) {
|
|
fetch_info("Using client cert file: %s",
|
|
client_cert_file);
|
|
fetch_info("Using client key file: %s",
|
|
client_key_file);
|
|
}
|
|
if (SSL_CTX_use_certificate_chain_file(ctx,
|
|
client_cert_file) != 1) {
|
|
fprintf(stderr,
|
|
"Could not load client certificate %s\n",
|
|
client_cert_file);
|
|
return (0);
|
|
}
|
|
if (SSL_CTX_use_PrivateKey_file(ctx, client_key_file,
|
|
SSL_FILETYPE_PEM) != 1) {
|
|
fprintf(stderr,
|
|
"Could not load client key %s\n",
|
|
client_key_file);
|
|
return (0);
|
|
}
|
|
}
|
|
return (1);
|
|
}
|
|
|
|
/*
|
|
* Callback for SSL certificate verification, this is called on server
|
|
* cert verification. It takes no decision, but informs the user in case
|
|
* verification failed.
|
|
*/
|
|
int
|
|
fetch_ssl_cb_verify_crt(int verified, X509_STORE_CTX *ctx)
|
|
{
|
|
X509 *crt;
|
|
X509_NAME *name;
|
|
char *str;
|
|
|
|
str = NULL;
|
|
if (!verified) {
|
|
if ((crt = X509_STORE_CTX_get_current_cert(ctx)) != NULL &&
|
|
(name = X509_get_subject_name(crt)) != NULL)
|
|
str = X509_NAME_oneline(name, 0, 0);
|
|
fprintf(stderr, "Certificate verification failed for %s\n",
|
|
str != NULL ? str : "no relevant certificate");
|
|
OPENSSL_free(str);
|
|
}
|
|
return (verified);
|
|
}
|
|
|
|
#endif
|
|
|
|
/*
|
|
* Enable SSL on a connection.
|
|
*/
|
|
int
|
|
fetch_ssl(conn_t *conn, const struct url *URL, int verbose)
|
|
{
|
|
#ifdef WITH_SSL
|
|
int ret, ssl_err;
|
|
X509_NAME *name;
|
|
char *str;
|
|
|
|
conn->ssl_meth = SSLv23_client_method();
|
|
conn->ssl_ctx = SSL_CTX_new(conn->ssl_meth);
|
|
SSL_CTX_set_mode(conn->ssl_ctx, SSL_MODE_AUTO_RETRY);
|
|
|
|
fetch_ssl_setup_transport_layer(conn->ssl_ctx, verbose);
|
|
if (!fetch_ssl_setup_peer_verification(conn->ssl_ctx, verbose))
|
|
return (-1);
|
|
if (!fetch_ssl_setup_client_certificate(conn->ssl_ctx, verbose))
|
|
return (-1);
|
|
|
|
conn->ssl = SSL_new(conn->ssl_ctx);
|
|
if (conn->ssl == NULL) {
|
|
fprintf(stderr, "SSL context creation failed\n");
|
|
return (-1);
|
|
}
|
|
SSL_set_fd(conn->ssl, conn->sd);
|
|
|
|
#if !defined(OPENSSL_NO_TLSEXT)
|
|
if (!SSL_set_tlsext_host_name(conn->ssl,
|
|
__DECONST(struct url *, URL)->host)) {
|
|
fprintf(stderr,
|
|
"TLS server name indication extension failed for host %s\n",
|
|
URL->host);
|
|
return (-1);
|
|
}
|
|
#endif
|
|
while ((ret = SSL_connect(conn->ssl)) == -1) {
|
|
ssl_err = SSL_get_error(conn->ssl, ret);
|
|
if (ssl_err != SSL_ERROR_WANT_READ &&
|
|
ssl_err != SSL_ERROR_WANT_WRITE) {
|
|
ERR_print_errors_fp(stderr);
|
|
return (-1);
|
|
}
|
|
}
|
|
conn->ssl_cert = SSL_get_peer_certificate(conn->ssl);
|
|
|
|
if (conn->ssl_cert == NULL) {
|
|
fprintf(stderr, "No server SSL certificate\n");
|
|
return (-1);
|
|
}
|
|
|
|
if (getenv("SSL_NO_VERIFY_HOSTNAME") == NULL) {
|
|
if (verbose)
|
|
fetch_info("Verify hostname");
|
|
if (!fetch_ssl_verify_hname(conn->ssl_cert, URL->host)) {
|
|
fprintf(stderr,
|
|
"SSL certificate subject doesn't match host %s\n",
|
|
URL->host);
|
|
return (-1);
|
|
}
|
|
}
|
|
|
|
if (verbose) {
|
|
fetch_info("%s connection established using %s",
|
|
SSL_get_version(conn->ssl), SSL_get_cipher(conn->ssl));
|
|
name = X509_get_subject_name(conn->ssl_cert);
|
|
str = X509_NAME_oneline(name, 0, 0);
|
|
fetch_info("Certificate subject: %s", str);
|
|
OPENSSL_free(str);
|
|
name = X509_get_issuer_name(conn->ssl_cert);
|
|
str = X509_NAME_oneline(name, 0, 0);
|
|
fetch_info("Certificate issuer: %s", str);
|
|
OPENSSL_free(str);
|
|
}
|
|
|
|
return (0);
|
|
#else
|
|
(void)conn;
|
|
(void)verbose;
|
|
(void)URL;
|
|
fprintf(stderr, "SSL support disabled\n");
|
|
return (-1);
|
|
#endif
|
|
}
|
|
|
|
#define FETCH_READ_WAIT -2
|
|
#define FETCH_READ_ERROR -1
|
|
#define FETCH_READ_DONE 0
|
|
|
|
#ifdef WITH_SSL
|
|
static ssize_t
|
|
fetch_ssl_read(SSL *ssl, char *buf, size_t len)
|
|
{
|
|
ssize_t rlen;
|
|
int ssl_err;
|
|
|
|
rlen = SSL_read(ssl, buf, len);
|
|
if (rlen < 0) {
|
|
ssl_err = SSL_get_error(ssl, rlen);
|
|
if (ssl_err == SSL_ERROR_WANT_READ ||
|
|
ssl_err == SSL_ERROR_WANT_WRITE) {
|
|
return (FETCH_READ_WAIT);
|
|
} else {
|
|
ERR_print_errors_fp(stderr);
|
|
return (FETCH_READ_ERROR);
|
|
}
|
|
}
|
|
return (rlen);
|
|
}
|
|
#endif
|
|
|
|
static ssize_t
|
|
fetch_socket_read(int sd, char *buf, size_t len)
|
|
{
|
|
ssize_t rlen;
|
|
|
|
rlen = read(sd, buf, len);
|
|
if (rlen < 0) {
|
|
if (errno == EAGAIN || (errno == EINTR && fetchRestartCalls))
|
|
return (FETCH_READ_WAIT);
|
|
else
|
|
return (FETCH_READ_ERROR);
|
|
}
|
|
return (rlen);
|
|
}
|
|
|
|
/*
|
|
* Read a character from a connection w/ timeout
|
|
*/
|
|
ssize_t
|
|
fetch_read(conn_t *conn, char *buf, size_t len)
|
|
{
|
|
struct timeval now, timeout, delta;
|
|
struct pollfd pfd;
|
|
ssize_t rlen;
|
|
int deltams;
|
|
|
|
if (fetchTimeout > 0) {
|
|
gettimeofday(&timeout, NULL);
|
|
timeout.tv_sec += fetchTimeout;
|
|
}
|
|
|
|
deltams = INFTIM;
|
|
memset(&pfd, 0, sizeof pfd);
|
|
pfd.fd = conn->sd;
|
|
pfd.events = POLLIN | POLLERR;
|
|
|
|
for (;;) {
|
|
/*
|
|
* The socket is non-blocking. Instead of the canonical
|
|
* poll() -> read(), we do the following:
|
|
*
|
|
* 1) call read() or SSL_read().
|
|
* 2) if we received some data, return it.
|
|
* 3) if an error occurred, return -1.
|
|
* 4) if read() or SSL_read() signaled EOF, return.
|
|
* 5) if we did not receive any data but we're not at EOF,
|
|
* call poll().
|
|
*
|
|
* In the SSL case, this is necessary because if we
|
|
* receive a close notification, we have to call
|
|
* SSL_read() one additional time after we've read
|
|
* everything we received.
|
|
*
|
|
* In the non-SSL case, it may improve performance (very
|
|
* slightly) when reading small amounts of data.
|
|
*/
|
|
#ifdef WITH_SSL
|
|
if (conn->ssl != NULL)
|
|
rlen = fetch_ssl_read(conn->ssl, buf, len);
|
|
else
|
|
#endif
|
|
rlen = fetch_socket_read(conn->sd, buf, len);
|
|
if (rlen >= 0) {
|
|
break;
|
|
} else if (rlen == FETCH_READ_ERROR) {
|
|
fetch_syserr();
|
|
return (-1);
|
|
}
|
|
// assert(rlen == FETCH_READ_WAIT);
|
|
if (fetchTimeout > 0) {
|
|
gettimeofday(&now, NULL);
|
|
if (!timercmp(&timeout, &now, >)) {
|
|
errno = ETIMEDOUT;
|
|
fetch_syserr();
|
|
return (-1);
|
|
}
|
|
timersub(&timeout, &now, &delta);
|
|
deltams = delta.tv_sec * 1000 +
|
|
delta.tv_usec / 1000;
|
|
}
|
|
errno = 0;
|
|
pfd.revents = 0;
|
|
if (poll(&pfd, 1, deltams) < 0) {
|
|
if (errno == EINTR && fetchRestartCalls)
|
|
continue;
|
|
fetch_syserr();
|
|
return (-1);
|
|
}
|
|
}
|
|
return (rlen);
|
|
}
|
|
|
|
|
|
/*
|
|
* Read a line of text from a connection w/ timeout
|
|
*/
|
|
#define MIN_BUF_SIZE 1024
|
|
|
|
int
|
|
fetch_getln(conn_t *conn)
|
|
{
|
|
char *tmp;
|
|
size_t tmpsize;
|
|
ssize_t len;
|
|
char c;
|
|
|
|
if (conn->buf == NULL) {
|
|
if ((conn->buf = malloc(MIN_BUF_SIZE)) == NULL) {
|
|
errno = ENOMEM;
|
|
return (-1);
|
|
}
|
|
conn->bufsize = MIN_BUF_SIZE;
|
|
}
|
|
|
|
conn->buf[0] = '\0';
|
|
conn->buflen = 0;
|
|
|
|
do {
|
|
len = fetch_read(conn, &c, 1);
|
|
if (len == -1)
|
|
return (-1);
|
|
if (len == 0)
|
|
break;
|
|
conn->buf[conn->buflen++] = c;
|
|
if (conn->buflen == conn->bufsize) {
|
|
tmp = conn->buf;
|
|
tmpsize = conn->bufsize * 2 + 1;
|
|
if ((tmp = realloc(tmp, tmpsize)) == NULL) {
|
|
errno = ENOMEM;
|
|
return (-1);
|
|
}
|
|
conn->buf = tmp;
|
|
conn->bufsize = tmpsize;
|
|
}
|
|
} while (c != '\n');
|
|
|
|
conn->buf[conn->buflen] = '\0';
|
|
DEBUGF("<<< %s", conn->buf);
|
|
return (0);
|
|
}
|
|
|
|
|
|
/*
|
|
* Write to a connection w/ timeout
|
|
*/
|
|
ssize_t
|
|
fetch_write(conn_t *conn, const char *buf, size_t len)
|
|
{
|
|
struct iovec iov;
|
|
|
|
iov.iov_base = __DECONST(char *, buf);
|
|
iov.iov_len = len;
|
|
return fetch_writev(conn, &iov, 1);
|
|
}
|
|
|
|
/*
|
|
* Write a vector to a connection w/ timeout
|
|
* Note: can modify the iovec.
|
|
*/
|
|
ssize_t
|
|
fetch_writev(conn_t *conn, struct iovec *iov, int iovcnt)
|
|
{
|
|
struct timeval now, timeout, delta;
|
|
struct pollfd pfd;
|
|
ssize_t wlen, total;
|
|
int deltams;
|
|
|
|
memset(&pfd, 0, sizeof pfd);
|
|
if (fetchTimeout) {
|
|
pfd.fd = conn->sd;
|
|
pfd.events = POLLOUT | POLLERR;
|
|
gettimeofday(&timeout, NULL);
|
|
timeout.tv_sec += fetchTimeout;
|
|
}
|
|
|
|
total = 0;
|
|
while (iovcnt > 0) {
|
|
while (fetchTimeout && pfd.revents == 0) {
|
|
gettimeofday(&now, NULL);
|
|
if (!timercmp(&timeout, &now, >)) {
|
|
errno = ETIMEDOUT;
|
|
fetch_syserr();
|
|
return (-1);
|
|
}
|
|
timersub(&timeout, &now, &delta);
|
|
deltams = delta.tv_sec * 1000 +
|
|
delta.tv_usec / 1000;
|
|
errno = 0;
|
|
pfd.revents = 0;
|
|
if (poll(&pfd, 1, deltams) < 0) {
|
|
/* POSIX compliance */
|
|
if (errno == EAGAIN)
|
|
continue;
|
|
if (errno == EINTR && fetchRestartCalls)
|
|
continue;
|
|
return (-1);
|
|
}
|
|
}
|
|
errno = 0;
|
|
#ifdef WITH_SSL
|
|
if (conn->ssl != NULL)
|
|
wlen = SSL_write(conn->ssl,
|
|
iov->iov_base, iov->iov_len);
|
|
else
|
|
#endif
|
|
wlen = writev(conn->sd, iov, iovcnt);
|
|
if (wlen == 0) {
|
|
/* we consider a short write a failure */
|
|
/* XXX perhaps we shouldn't in the SSL case */
|
|
errno = EPIPE;
|
|
fetch_syserr();
|
|
return (-1);
|
|
}
|
|
if (wlen < 0) {
|
|
if (errno == EINTR && fetchRestartCalls)
|
|
continue;
|
|
return (-1);
|
|
}
|
|
total += wlen;
|
|
while (iovcnt > 0 && wlen >= (ssize_t)iov->iov_len) {
|
|
wlen -= iov->iov_len;
|
|
iov++;
|
|
iovcnt--;
|
|
}
|
|
if (iovcnt > 0) {
|
|
iov->iov_len -= wlen;
|
|
iov->iov_base = __DECONST(char *, iov->iov_base) + wlen;
|
|
}
|
|
}
|
|
return (total);
|
|
}
|
|
|
|
|
|
/*
|
|
* Write a line of text to a connection w/ timeout
|
|
*/
|
|
int
|
|
fetch_putln(conn_t *conn, const char *str, size_t len)
|
|
{
|
|
struct iovec iov[2];
|
|
int ret;
|
|
|
|
DEBUGF(">>> %s\n", str);
|
|
iov[0].iov_base = __DECONST(char *, str);
|
|
iov[0].iov_len = len;
|
|
iov[1].iov_base = __DECONST(char *, ENDL);
|
|
iov[1].iov_len = sizeof(ENDL);
|
|
if (len == 0)
|
|
ret = fetch_writev(conn, &iov[1], 1);
|
|
else
|
|
ret = fetch_writev(conn, iov, 2);
|
|
if (ret == -1)
|
|
return (-1);
|
|
return (0);
|
|
}
|
|
|
|
|
|
/*
|
|
* Close connection
|
|
*/
|
|
int
|
|
fetch_close(conn_t *conn)
|
|
{
|
|
int ret;
|
|
|
|
if (--conn->ref > 0)
|
|
return (0);
|
|
#ifdef WITH_SSL
|
|
if (conn->ssl) {
|
|
SSL_shutdown(conn->ssl);
|
|
SSL_set_connect_state(conn->ssl);
|
|
SSL_free(conn->ssl);
|
|
conn->ssl = NULL;
|
|
}
|
|
if (conn->ssl_ctx) {
|
|
SSL_CTX_free(conn->ssl_ctx);
|
|
conn->ssl_ctx = NULL;
|
|
}
|
|
if (conn->ssl_cert) {
|
|
X509_free(conn->ssl_cert);
|
|
conn->ssl_cert = NULL;
|
|
}
|
|
#endif
|
|
ret = close(conn->sd);
|
|
free(conn->buf);
|
|
free(conn);
|
|
return (ret);
|
|
}
|
|
|
|
|
|
/*** Directory-related utility functions *************************************/
|
|
|
|
int
|
|
fetch_add_entry(struct url_ent **p, int *size, int *len,
|
|
const char *name, struct url_stat *us)
|
|
{
|
|
struct url_ent *tmp;
|
|
|
|
if (*p == NULL) {
|
|
*size = 0;
|
|
*len = 0;
|
|
}
|
|
|
|
if (*len >= *size - 1) {
|
|
tmp = reallocarray(*p, *size * 2 + 1, sizeof(**p));
|
|
if (tmp == NULL) {
|
|
errno = ENOMEM;
|
|
fetch_syserr();
|
|
return (-1);
|
|
}
|
|
*size = (*size * 2 + 1);
|
|
*p = tmp;
|
|
}
|
|
|
|
tmp = *p + *len;
|
|
snprintf(tmp->name, PATH_MAX, "%s", name);
|
|
memcpy(&tmp->stat, us, sizeof(*us));
|
|
|
|
(*len)++;
|
|
(++tmp)->name[0] = 0;
|
|
|
|
return (0);
|
|
}
|
|
|
|
|
|
/*** Authentication-related utility functions ********************************/
|
|
|
|
static const char *
|
|
fetch_read_word(FILE *f)
|
|
{
|
|
static char word[1024];
|
|
|
|
if (fscanf(f, " %1023s ", word) != 1)
|
|
return (NULL);
|
|
return (word);
|
|
}
|
|
|
|
static int
|
|
fetch_netrc_open(void)
|
|
{
|
|
struct passwd *pwd;
|
|
char fn[PATH_MAX];
|
|
const char *p;
|
|
int fd, serrno;
|
|
|
|
if ((p = getenv("NETRC")) != NULL) {
|
|
DEBUGF("NETRC=%s\n", p);
|
|
if (snprintf(fn, sizeof(fn), "%s", p) >= (int)sizeof(fn)) {
|
|
fetch_info("$NETRC specifies a file name "
|
|
"longer than PATH_MAX");
|
|
return (-1);
|
|
}
|
|
} else {
|
|
if ((p = getenv("HOME")) == NULL) {
|
|
if ((pwd = getpwuid(getuid())) == NULL ||
|
|
(p = pwd->pw_dir) == NULL)
|
|
return (-1);
|
|
}
|
|
if (snprintf(fn, sizeof(fn), "%s/.netrc", p) >= (int)sizeof(fn))
|
|
return (-1);
|
|
}
|
|
|
|
if ((fd = open(fn, O_RDONLY)) < 0) {
|
|
serrno = errno;
|
|
DEBUGF("%s: %s\n", fn, strerror(serrno));
|
|
errno = serrno;
|
|
}
|
|
return (fd);
|
|
}
|
|
|
|
/*
|
|
* Get authentication data for a URL from .netrc
|
|
*/
|
|
int
|
|
fetch_netrc_auth(struct url *url)
|
|
{
|
|
const char *word;
|
|
int serrno;
|
|
FILE *f;
|
|
|
|
if (url->netrcfd < 0)
|
|
url->netrcfd = fetch_netrc_open();
|
|
if (url->netrcfd < 0)
|
|
return (-1);
|
|
if ((f = fdopen(url->netrcfd, "r")) == NULL) {
|
|
serrno = errno;
|
|
DEBUGF("fdopen(netrcfd): %s", strerror(errno));
|
|
close(url->netrcfd);
|
|
url->netrcfd = -1;
|
|
errno = serrno;
|
|
return (-1);
|
|
}
|
|
rewind(f);
|
|
DEBUGF("searching netrc for %s\n", url->host);
|
|
while ((word = fetch_read_word(f)) != NULL) {
|
|
if (strcmp(word, "default") == 0) {
|
|
DEBUGF("using default netrc settings\n");
|
|
break;
|
|
}
|
|
if (strcmp(word, "machine") == 0 &&
|
|
(word = fetch_read_word(f)) != NULL &&
|
|
strcasecmp(word, url->host) == 0) {
|
|
DEBUGF("using netrc settings for %s\n", word);
|
|
break;
|
|
}
|
|
}
|
|
if (word == NULL)
|
|
goto ferr;
|
|
while ((word = fetch_read_word(f)) != NULL) {
|
|
if (strcmp(word, "login") == 0) {
|
|
if ((word = fetch_read_word(f)) == NULL)
|
|
goto ferr;
|
|
if (snprintf(url->user, sizeof(url->user),
|
|
"%s", word) > (int)sizeof(url->user)) {
|
|
fetch_info("login name in .netrc is too long");
|
|
url->user[0] = '\0';
|
|
}
|
|
} else if (strcmp(word, "password") == 0) {
|
|
if ((word = fetch_read_word(f)) == NULL)
|
|
goto ferr;
|
|
if (snprintf(url->pwd, sizeof(url->pwd),
|
|
"%s", word) > (int)sizeof(url->pwd)) {
|
|
fetch_info("password in .netrc is too long");
|
|
url->pwd[0] = '\0';
|
|
}
|
|
} else if (strcmp(word, "account") == 0) {
|
|
if ((word = fetch_read_word(f)) == NULL)
|
|
goto ferr;
|
|
/* XXX not supported! */
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
fclose(f);
|
|
url->netrcfd = -1;
|
|
return (0);
|
|
ferr:
|
|
serrno = errno;
|
|
fclose(f);
|
|
url->netrcfd = -1;
|
|
errno = serrno;
|
|
return (-1);
|
|
}
|
|
|
|
/*
|
|
* The no_proxy environment variable specifies a set of domains for
|
|
* which the proxy should not be consulted; the contents is a comma-,
|
|
* or space-separated list of domain names. A single asterisk will
|
|
* override all proxy variables and no transactions will be proxied
|
|
* (for compatibility with lynx and curl, see the discussion at
|
|
* <http://curl.haxx.se/mail/archive_pre_oct_99/0009.html>).
|
|
*/
|
|
int
|
|
fetch_no_proxy_match(const char *host)
|
|
{
|
|
const char *no_proxy, *p, *q;
|
|
size_t h_len, d_len;
|
|
|
|
if ((no_proxy = getenv("NO_PROXY")) == NULL &&
|
|
(no_proxy = getenv("no_proxy")) == NULL)
|
|
return (0);
|
|
|
|
/* asterisk matches any hostname */
|
|
if (strcmp(no_proxy, "*") == 0)
|
|
return (1);
|
|
|
|
h_len = strlen(host);
|
|
p = no_proxy;
|
|
do {
|
|
/* position p at the beginning of a domain suffix */
|
|
while (*p == ',' || isspace((unsigned char)*p))
|
|
p++;
|
|
|
|
/* position q at the first separator character */
|
|
for (q = p; *q; ++q)
|
|
if (*q == ',' || isspace((unsigned char)*q))
|
|
break;
|
|
|
|
d_len = q - p;
|
|
if (d_len > 0 && h_len >= d_len &&
|
|
strncasecmp(host + h_len - d_len,
|
|
p, d_len) == 0) {
|
|
/* domain name matches */
|
|
return (1);
|
|
}
|
|
|
|
p = q + 1;
|
|
} while (*q);
|
|
|
|
return (0);
|
|
}
|