From eee068778cb28ecf3c14e1bf843a95547d72c42d Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 18:14:06 +0200 Subject: Adding upstream version 2.2.40. Signed-off-by: Daniel Baumann --- dirmngr/http.c | 3798 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 3798 insertions(+) create mode 100644 dirmngr/http.c (limited to 'dirmngr/http.c') diff --git a/dirmngr/http.c b/dirmngr/http.c new file mode 100644 index 0000000..946234e --- /dev/null +++ b/dirmngr/http.c @@ -0,0 +1,3798 @@ +/* http.c - HTTP protocol handler + * Copyright (C) 1999, 2001-2004, 2006, 2009, 2010, + * 2011 Free Software Foundation, Inc. + * Copyright (C) 1999, 2001-2004, 2006, 2009, 2010, 2011, 2014 Werner Koch + * Copyright (C) 2015-2017, 2021 g10 Code GmbH + * + * This file is part of GnuPG. + * + * This file is free software; you can redistribute it and/or modify + * it under the terms of either + * + * - the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 3 of the License, or (at + * your option) any later version. + * + * or + * + * - the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * or both in parallel, as here. + * + * This file is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + */ + +/* Simple HTTP client implementation. We try to keep the code as + self-contained as possible. There are some constraints however: + + - estream is required. We now require estream because it provides a + very useful and portable asprintf implementation and the fopencookie + function. + - stpcpy is required + - fixme: list other requirements. + + + - With HTTP_USE_NTBTLS or HTTP_USE_GNUTLS support for https is + provided (this also requires estream). + + - With HTTP_NO_WSASTARTUP the socket initialization is not done + under Windows. This is useful if the socket layer has already + been initialized elsewhere. This also avoids the installation of + an exit handler to cleanup the socket layer. +*/ + +#ifdef HAVE_CONFIG_H +# include +#endif +#include +#include +#include +#include +#include +#include +#include + +#ifdef HAVE_W32_SYSTEM +# ifdef HAVE_WINSOCK2_H +# include +# endif +# include +#else /*!HAVE_W32_SYSTEM*/ +# include +# include +# include +# include +# include +# include +# include +# include +#endif /*!HAVE_W32_SYSTEM*/ + +#ifdef WITHOUT_NPTH /* Give the Makefile a chance to build without Pth. */ +# undef USE_NPTH +#endif + +#ifdef USE_NPTH +# include +#endif + +#if defined (HTTP_USE_GNUTLS) && defined (HTTP_USE_NTBTLS) +# error Both, HTTP_USE_GNUTLS and HTTP_USE_NTBTLS, are defined. +#endif + +#ifdef HTTP_USE_NTBTLS +# include +#elif HTTP_USE_GNUTLS +# include +# include +#endif /*HTTP_USE_GNUTLS*/ + +#include /* We need the socket wrapper. */ + +#include "../common/util.h" +#include "../common/i18n.h" +#include "../common/sysutils.h" /* (gnupg_fd_t) */ +#include "dns-stuff.h" +#include "dirmngr-status.h" /* (dirmngr_status_printf) */ +#include "http.h" +#include "http-common.h" + + +#ifdef USE_NPTH +# define my_select(a,b,c,d,e) npth_select ((a), (b), (c), (d), (e)) +# define my_accept(a,b,c) npth_accept ((a), (b), (c)) +#else +# define my_select(a,b,c,d,e) select ((a), (b), (c), (d), (e)) +# define my_accept(a,b,c) accept ((a), (b), (c)) +#endif + +#ifdef HAVE_W32_SYSTEM +#define sock_close(a) closesocket(a) +#else +#define sock_close(a) close(a) +#endif + +#ifndef EAGAIN +#define EAGAIN EWOULDBLOCK +#endif +#ifndef INADDR_NONE /* Slowaris is missing that. */ +#define INADDR_NONE ((unsigned long)(-1)) +#endif /*INADDR_NONE*/ + +#define HTTP_PROXY_ENV "http_proxy" +#define MAX_LINELEN 20000 /* Max. length of a HTTP header line. */ +#define VALID_URI_CHARS "abcdefghijklmnopqrstuvwxyz" \ + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" \ + "01234567890@" \ + "!\"#$%&'()*+,-./:;<=>?[\\]^_{|}~" + +#if HTTP_USE_NTBTLS +typedef ntbtls_t tls_session_t; +# define USE_TLS 1 +#elif HTTP_USE_GNUTLS +typedef gnutls_session_t tls_session_t; +# define USE_TLS 1 +#else +typedef void *tls_session_t; +# undef USE_TLS +#endif + +static gpg_err_code_t do_parse_uri (parsed_uri_t uri, int only_local_part, + int no_scheme_check, int force_tls); +static gpg_error_t parse_uri (parsed_uri_t *ret_uri, const char *uri, + int no_scheme_check, int force_tls); +static int remove_escapes (char *string); +static int insert_escapes (char *buffer, const char *string, + const char *special); +static uri_tuple_t parse_tuple (char *string); +static gpg_error_t send_request (http_t hd, const char *httphost, + const char *auth,const char *proxy, + const char *srvtag, unsigned int timeout, + strlist_t headers); +static char *build_rel_path (parsed_uri_t uri); +static gpg_error_t parse_response (http_t hd); + +static gpg_error_t connect_server (const char *server, unsigned short port, + unsigned int flags, const char *srvtag, + unsigned int timeout, assuan_fd_t *r_sock); +static gpgrt_ssize_t read_server (assuan_fd_t sock, void *buffer, size_t size); +static gpg_error_t write_server (assuan_fd_t sock, const char *data, size_t length); + +static gpgrt_ssize_t cookie_read (void *cookie, void *buffer, size_t size); +static gpgrt_ssize_t cookie_write (void *cookie, + const void *buffer, size_t size); +static int cookie_close (void *cookie); +#if defined(HAVE_W32_SYSTEM) && defined(HTTP_USE_NTBTLS) +static gpgrt_ssize_t simple_cookie_read (void *cookie, + void *buffer, size_t size); +static gpgrt_ssize_t simple_cookie_write (void *cookie, + const void *buffer, size_t size); +#endif + +/* A socket object used to a allow ref counting of sockets. */ +struct my_socket_s +{ + assuan_fd_t fd; /* The actual socket - shall never be ASSUAN_INVALID_FD. */ + int refcount; /* Number of references to this socket. */ +}; +typedef struct my_socket_s *my_socket_t; + + +/* Cookie function structure and cookie object. */ +static es_cookie_io_functions_t cookie_functions = + { + cookie_read, + cookie_write, + NULL, + cookie_close + }; + + +struct cookie_s +{ + /* Socket object or NULL if already closed. */ + my_socket_t sock; + + /* The session object or NULL if not used. */ + http_session_t session; + + /* True if TLS is to be used. */ + int use_tls; + + /* The remaining content length and a flag telling whether to use + the content length. */ + uint64_t content_length; + unsigned int content_length_valid:1; +}; +typedef struct cookie_s *cookie_t; + + +/* Simple cookie functions. Here the cookie is an int with the + * socket. */ +#if defined(HAVE_W32_SYSTEM) && defined(HTTP_USE_NTBTLS) +static es_cookie_io_functions_t simple_cookie_functions = + { + simple_cookie_read, + simple_cookie_write, + NULL, + NULL + }; +#endif + + +#if SIZEOF_UNSIGNED_LONG == 8 +# define HTTP_SESSION_MAGIC 0x0068545470534553 /* "hTTpSES" */ +#else +# define HTTP_SESSION_MAGIC 0x68547365 /* "hTse" */ +#endif + +/* The session object. */ +struct http_session_s +{ + unsigned long magic; + + int refcount; /* Number of references to this object. */ +#ifdef HTTP_USE_GNUTLS + gnutls_certificate_credentials_t certcred; +#endif /*HTTP_USE_GNUTLS*/ +#ifdef USE_TLS + tls_session_t tls_session; + struct { + int done; /* Verifciation has been done. */ + int rc; /* TLS verification return code. */ + unsigned int status; /* Verification status. */ + } verify; + char *servername; /* Malloced server name. */ +#endif /*USE_TLS*/ + /* A callback function to log details of TLS certifciates. */ + void (*cert_log_cb) (http_session_t, gpg_error_t, const char *, + const void **, size_t *); + + /* The flags passed to the session object. */ + unsigned int flags; + + /* A per-session TLS verification callback. */ + http_verify_cb_t verify_cb; + void *verify_cb_value; + + /* The connect timeout */ + unsigned int connect_timeout; +}; + + +/* An object to save header lines. */ +struct header_s +{ + struct header_s *next; + char *value; /* The value of the header (malloced). */ + char name[1]; /* The name of the header (canonicalized). */ +}; +typedef struct header_s *header_t; + + +#if SIZEOF_UNSIGNED_LONG == 8 +# define HTTP_CONTEXT_MAGIC 0x0068545470435458 /* "hTTpCTX" */ +#else +# define HTTP_CONTEXT_MAGIC 0x68546378 /* "hTcx" */ +#endif + + +/* Our handle context. */ +struct http_context_s +{ + unsigned long magic; + unsigned int status_code; + my_socket_t sock; + unsigned int in_data:1; + unsigned int is_http_0_9:1; + estream_t fp_read; + estream_t fp_write; + void *write_cookie; + void *read_cookie; + http_session_t session; + parsed_uri_t uri; + http_req_t req_type; + char *buffer; /* Line buffer. */ + size_t buffer_size; + unsigned int flags; + header_t headers; /* Received headers. */ +}; + + +/* Two flags to enable verbose and debug mode. Although currently not + * set-able a value > 1 for OPT_DEBUG enables debugging of the session + * reference counting. */ +static int opt_verbose; +static int opt_debug; + +/* The global callback for the verification function. */ +static gpg_error_t (*tls_callback) (http_t, http_session_t, int); + +/* The list of files with trusted CA certificates. */ +static strlist_t tls_ca_certlist; + +/* The list of files with extra trusted CA certificates. */ +static strlist_t cfg_ca_certlist; + +/* The global callback for net activity. */ +static void (*netactivity_cb)(void); + + + +#if defined(HAVE_W32_SYSTEM) && !defined(HTTP_NO_WSASTARTUP) + +#if GNUPG_MAJOR_VERSION == 1 +#define REQ_WINSOCK_MAJOR 1 +#define REQ_WINSOCK_MINOR 1 +#else +#define REQ_WINSOCK_MAJOR 2 +#define REQ_WINSOCK_MINOR 2 +#endif + + +static void +deinit_sockets (void) +{ + WSACleanup(); +} + +static void +init_sockets (void) +{ + static int initialized; + static WSADATA wsdata; + + if (initialized) + return; + + if ( WSAStartup( MAKEWORD (REQ_WINSOCK_MINOR, REQ_WINSOCK_MAJOR), &wsdata ) ) + { + log_error ("error initializing socket library: ec=%d\n", + (int)WSAGetLastError () ); + return; + } + if ( LOBYTE(wsdata.wVersion) != REQ_WINSOCK_MAJOR + || HIBYTE(wsdata.wVersion) != REQ_WINSOCK_MINOR ) + { + log_error ("socket library version is %x.%x - but %d.%d needed\n", + LOBYTE(wsdata.wVersion), HIBYTE(wsdata.wVersion), + REQ_WINSOCK_MAJOR, REQ_WINSOCK_MINOR); + WSACleanup(); + return; + } + atexit ( deinit_sockets ); + initialized = 1; +} +#endif /*HAVE_W32_SYSTEM && !HTTP_NO_WSASTARTUP*/ + + +/* Create a new socket object. Returns NULL and closes FD if not + enough memory is available. */ +static my_socket_t +_my_socket_new (int lnr, assuan_fd_t fd) +{ + my_socket_t so; + + so = xtrymalloc (sizeof *so); + if (!so) + { + int save_errno = errno; + assuan_sock_close (fd); + gpg_err_set_errno (save_errno); + return NULL; + } + so->fd = fd; + so->refcount = 1; + if (opt_debug) + log_debug ("http.c:%d:socket_new: object %p for fd %d created\n", + lnr, so, (int)so->fd); + return so; +} +#define my_socket_new(a) _my_socket_new (__LINE__, (a)) + +/* Bump up the reference counter for the socket object SO. */ +static my_socket_t +_my_socket_ref (int lnr, my_socket_t so) +{ + so->refcount++; + if (opt_debug > 1) + log_debug ("http.c:%d:socket_ref: object %p for fd %d refcount now %d\n", + lnr, so, (int)so->fd, so->refcount); + return so; +} +#define my_socket_ref(a) _my_socket_ref (__LINE__,(a)) + + +/* Bump down the reference counter for the socket object SO. If SO + has no more references, close the socket and release the + object. */ +static void +_my_socket_unref (int lnr, my_socket_t so, + void (*preclose)(void*), void *preclosearg) +{ + if (so) + { + so->refcount--; + if (opt_debug > 1) + log_debug ("http.c:%d:socket_unref: object %p for fd %d ref now %d\n", + lnr, so, (int)so->fd, so->refcount); + + if (!so->refcount) + { + if (preclose) + preclose (preclosearg); + assuan_sock_close (so->fd); + xfree (so); + } + } +} +#define my_socket_unref(a,b,c) _my_socket_unref (__LINE__,(a),(b),(c)) + + +#ifdef HTTP_USE_GNUTLS +static ssize_t +my_gnutls_read (gnutls_transport_ptr_t ptr, void *buffer, size_t size) +{ + my_socket_t sock = ptr; +#if USE_NPTH + return npth_read (sock->fd, buffer, size); +#else + return read (sock->fd, buffer, size); +#endif +} +static ssize_t +my_gnutls_write (gnutls_transport_ptr_t ptr, const void *buffer, size_t size) +{ + my_socket_t sock = ptr; +#if USE_NPTH + return npth_write (sock->fd, buffer, size); +#else + return write (sock->fd, buffer, size); +#endif +} +#endif /*HTTP_USE_GNUTLS*/ + + +#ifdef HTTP_USE_NTBTLS +/* Connect the ntbls callback to our generic callback. */ +static gpg_error_t +my_ntbtls_verify_cb (void *opaque, ntbtls_t tls, unsigned int verify_flags) +{ + http_t hd = opaque; + + (void)verify_flags; + + log_assert (hd && hd->session && hd->session->verify_cb); + log_assert (hd->magic == HTTP_CONTEXT_MAGIC); + log_assert (hd->session->magic == HTTP_SESSION_MAGIC); + + return hd->session->verify_cb (hd->session->verify_cb_value, + hd, hd->session, + (hd->flags | hd->session->flags), + tls); +} +#endif /*HTTP_USE_NTBTLS*/ + + + + +/* This notification function is called by estream whenever stream is + closed. Its purpose is to mark the closing in the handle so + that a http_close won't accidentally close the estream. The function + http_close removes this notification so that it won't be called if + http_close was used before an es_fclose. */ +static void +fp_onclose_notification (estream_t stream, void *opaque) +{ + http_t hd = opaque; + + log_assert (hd->magic == HTTP_CONTEXT_MAGIC); + if (hd->fp_read && hd->fp_read == stream) + hd->fp_read = NULL; + else if (hd->fp_write && hd->fp_write == stream) + hd->fp_write = NULL; +} + + +/* + * Helper function to create an HTTP header with hex encoded data. A + * new buffer is returned. This buffer is the concatenation of the + * string PREFIX, the hex-encoded DATA of length LEN and the string + * SUFFIX. On error NULL is returned and ERRNO set. + */ +static char * +make_header_line (const char *prefix, const char *suffix, + const void *data, size_t len ) +{ + static unsigned char bintoasc[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789+/"; + const unsigned char *s = data; + char *buffer, *p; + + buffer = xtrymalloc (strlen (prefix) + (len+2)/3*4 + strlen (suffix) + 1); + if (!buffer) + return NULL; + p = stpcpy (buffer, prefix); + for ( ; len >= 3 ; len -= 3, s += 3 ) + { + *p++ = bintoasc[(s[0] >> 2) & 077]; + *p++ = bintoasc[(((s[0] <<4)&060)|((s[1] >> 4)&017))&077]; + *p++ = bintoasc[(((s[1]<<2)&074)|((s[2]>>6)&03))&077]; + *p++ = bintoasc[s[2]&077]; + *p = 0; + } + if ( len == 2 ) + { + *p++ = bintoasc[(s[0] >> 2) & 077]; + *p++ = bintoasc[(((s[0] <<4)&060)|((s[1] >> 4)&017))&077]; + *p++ = bintoasc[((s[1]<<2)&074)]; + *p++ = '='; + } + else if ( len == 1 ) + { + *p++ = bintoasc[(s[0] >> 2) & 077]; + *p++ = bintoasc[(s[0] <<4)&060]; + *p++ = '='; + *p++ = '='; + } + *p = 0; + strcpy (p, suffix); + return buffer; +} + + + + +/* Set verbosity and debug mode for this module. */ +void +http_set_verbose (int verbose, int debug) +{ + opt_verbose = verbose; + opt_debug = debug; +} + + +/* Register a non-standard global TLS callback function. If no + verification is desired a callback needs to be registered which + always returns NULL. */ +void +http_register_tls_callback (gpg_error_t (*cb)(http_t, http_session_t, int)) +{ + tls_callback = cb; +} + + +/* Register a CA certificate for future use. The certificate is + expected to be in FNAME. PEM format is assume if FNAME has a + suffix of ".pem". If FNAME is NULL the list of CA files is + removed. */ +void +http_register_tls_ca (const char *fname) +{ + gpg_err_code_t ec; + strlist_t sl; + + if (!fname) + { + free_strlist (tls_ca_certlist); + tls_ca_certlist = NULL; + } + else + { + /* Warn if we can't access right now, but register it anyway in + case it becomes accessible later */ + if ((ec = gnupg_access (fname, F_OK))) + log_info (_("can't access '%s': %s\n"), fname, gpg_strerror (ec)); + sl = add_to_strlist (&tls_ca_certlist, fname); + if (*sl->d && !strcmp (sl->d + strlen (sl->d) - 4, ".pem")) + sl->flags = 1; + } +} + + +/* Register a CA certificate for future use. The certificate is + * expected to be in FNAME. PEM format is assume if FNAME has a + * suffix of ".pem". If FNAME is NULL the list of CA files is + * removed. This is a variant of http_register_tls_ca which puts the + * certificate into a separate list enabled using HTTP_FLAG_TRUST_CFG. */ +void +http_register_cfg_ca (const char *fname) +{ + gpg_err_code_t ec; + strlist_t sl; + + if (!fname) + { + free_strlist (cfg_ca_certlist); + cfg_ca_certlist = NULL; + } + else + { + /* Warn if we can't access right now, but register it anyway in + case it becomes accessible later */ + if ((ec = gnupg_access (fname, F_OK))) + log_info (_("can't access '%s': %s\n"), fname, gpg_strerror (ec)); + sl = add_to_strlist (&cfg_ca_certlist, fname); + if (*sl->d && !strcmp (sl->d + strlen (sl->d) - 4, ".pem")) + sl->flags = 1; + } +} + + +/* Register a callback which is called every time the HTTP mode has + * made a successful connection to some server. */ +void +http_register_netactivity_cb (void (*cb)(void)) +{ + netactivity_cb = cb; +} + + +/* Call the netactivity callback if any. */ +static void +notify_netactivity (void) +{ + if (netactivity_cb) + netactivity_cb (); +} + + + +#ifdef USE_TLS +/* Free the TLS session associated with SESS, if any. */ +static void +close_tls_session (http_session_t sess) +{ + if (sess->tls_session) + { +# if HTTP_USE_NTBTLS + /* FIXME!! + Possibly, ntbtls_get_transport and close those streams. + Somehow get SOCK to call my_socket_unref. + */ + ntbtls_release (sess->tls_session); +# elif HTTP_USE_GNUTLS + my_socket_t sock = gnutls_transport_get_ptr (sess->tls_session); + my_socket_unref (sock, NULL, NULL); + gnutls_deinit (sess->tls_session); + if (sess->certcred) + gnutls_certificate_free_credentials (sess->certcred); +# endif /*HTTP_USE_GNUTLS*/ + xfree (sess->servername); + sess->tls_session = NULL; + } +} +#endif /*USE_TLS*/ + + +/* Release a session. Take care not to release it while it is being + used by a http context object. */ +static void +session_unref (int lnr, http_session_t sess) +{ + if (!sess) + return; + + log_assert (sess->magic == HTTP_SESSION_MAGIC); + + sess->refcount--; + if (opt_debug > 1) + log_debug ("http.c:%d:session_unref: sess %p ref now %d\n", + lnr, sess, sess->refcount); + if (sess->refcount) + return; + +#ifdef USE_TLS + close_tls_session (sess); +#endif /*USE_TLS*/ + + sess->magic = 0xdeadbeef; + xfree (sess); +} +#define http_session_unref(a) session_unref (__LINE__, (a)) + +void +http_session_release (http_session_t sess) +{ + http_session_unref (sess); +} + + +/* Create a new session object which is currently used to enable TLS + * support. It may eventually allow reusing existing connections. + * Valid values for FLAGS are: + * HTTP_FLAG_TRUST_DEF - Use the CAs set with http_register_tls_ca + * HTTP_FLAG_TRUST_SYS - Also use the CAs defined by the system + * HTTP_FLAG_TRUST_CFG - Also use CAs set with http_register_cfg_ca + * HTTP_FLAG_NO_CRL - Do not consult CRLs for https. + */ +gpg_error_t +http_session_new (http_session_t *r_session, + const char *intended_hostname, unsigned int flags, + http_verify_cb_t verify_cb, void *verify_cb_value) +{ + gpg_error_t err; + http_session_t sess; + + *r_session = NULL; + + sess = xtrycalloc (1, sizeof *sess); + if (!sess) + return gpg_error_from_syserror (); + sess->magic = HTTP_SESSION_MAGIC; + sess->refcount = 1; + sess->flags = flags; + sess->verify_cb = verify_cb; + sess->verify_cb_value = verify_cb_value; + sess->connect_timeout = 0; + +#if HTTP_USE_NTBTLS + { + (void)intended_hostname; /* Not needed because we do not preload + * certificates. */ + + err = ntbtls_new (&sess->tls_session, NTBTLS_CLIENT); + if (err) + { + log_error ("ntbtls_new failed: %s\n", gpg_strerror (err)); + goto leave; + } + + } +#elif HTTP_USE_GNUTLS + { + const char *errpos; + int rc; + strlist_t sl; + int add_system_cas = !!(flags & HTTP_FLAG_TRUST_SYS); + int is_hkps_pool; + + rc = gnutls_certificate_allocate_credentials (&sess->certcred); + if (rc < 0) + { + log_error ("gnutls_certificate_allocate_credentials failed: %s\n", + gnutls_strerror (rc)); + err = gpg_error (GPG_ERR_GENERAL); + goto leave; + } + + /* Disabled for 2.2.19 to due problems with the standard hkps pool. */ + /* is_hkps_pool = (intended_hostname */ + /* && !ascii_strcasecmp (intended_hostname, */ + /* get_default_keyserver (1))); */ + is_hkps_pool = 0; + + /* If we are looking for the hkps pool from sks-keyservers.net, + * then forcefully use its dedicated certificate authority. */ + /* Disabled for 2.2.29 because the service had to be shutdown. */ + /* if (is_hkps_pool) */ + /* { */ + /* char *pemname = make_filename_try (gnupg_datadir (), */ + /* "sks-keyservers.netCA.pem", NULL); */ + /* if (!pemname) */ + /* { */ + /* err = gpg_error_from_syserror (); */ + /* log_error ("setting CA from file '%s' failed: %s\n", */ + /* pemname, gpg_strerror (err)); */ + /* } */ + /* else */ + /* { */ + /* rc = gnutls_certificate_set_x509_trust_file */ + /* (sess->certcred, pemname, GNUTLS_X509_FMT_PEM); */ + /* if (rc < 0) */ + /* log_info ("setting CA from file '%s' failed: %s\n", */ + /* pemname, gnutls_strerror (rc)); */ + /* xfree (pemname); */ + /* } */ + /* */ + /* if (is_hkps_pool) */ + /* add_system_cas = 0; */ + /* } */ + + /* Add configured certificates to the session. */ + if ((flags & HTTP_FLAG_TRUST_DEF) && !is_hkps_pool) + { + for (sl = tls_ca_certlist; sl; sl = sl->next) + { + rc = gnutls_certificate_set_x509_trust_file + (sess->certcred, sl->d, + (sl->flags & 1)? GNUTLS_X509_FMT_PEM : GNUTLS_X509_FMT_DER); + if (rc < 0) + log_info ("setting CA from file '%s' failed: %s\n", + sl->d, gnutls_strerror (rc)); + } + + /* If HKP trust is requested and there are no HKP certificates + * configured, also try the standard system certificates. */ + if (!tls_ca_certlist) + add_system_cas = 1; + } + + /* Add system certificates to the session. */ + if (add_system_cas) + { +#if GNUTLS_VERSION_NUMBER >= 0x030014 + static int shown; + + rc = gnutls_certificate_set_x509_system_trust (sess->certcred); + if (rc < 0) + log_info ("setting system CAs failed: %s\n", gnutls_strerror (rc)); + else if (!shown) + { + shown = 1; + log_info ("number of system provided CAs: %d\n", rc); + } +#endif /* gnutls >= 3.0.20 */ + } + + /* Add other configured certificates to the session. */ + if ((flags & HTTP_FLAG_TRUST_CFG) && !is_hkps_pool) + { + for (sl = cfg_ca_certlist; sl; sl = sl->next) + { + rc = gnutls_certificate_set_x509_trust_file + (sess->certcred, sl->d, + (sl->flags & 1)? GNUTLS_X509_FMT_PEM : GNUTLS_X509_FMT_DER); + if (rc < 0) + log_info ("setting extra CA from file '%s' failed: %s\n", + sl->d, gnutls_strerror (rc)); + } + } + + + rc = gnutls_init (&sess->tls_session, GNUTLS_CLIENT); + if (rc < 0) + { + log_error ("gnutls_init failed: %s\n", gnutls_strerror (rc)); + err = gpg_error (GPG_ERR_GENERAL); + goto leave; + } + /* A new session has the transport ptr set to (void*(-1), we need + it to be NULL. */ + gnutls_transport_set_ptr (sess->tls_session, NULL); + + rc = gnutls_priority_set_direct (sess->tls_session, + "NORMAL", + &errpos); + if (rc < 0) + { + log_error ("gnutls_priority_set_direct failed at '%s': %s\n", + errpos, gnutls_strerror (rc)); + err = gpg_error (GPG_ERR_GENERAL); + goto leave; + } + + rc = gnutls_credentials_set (sess->tls_session, + GNUTLS_CRD_CERTIFICATE, sess->certcred); + if (rc < 0) + { + log_error ("gnutls_credentials_set failed: %s\n", gnutls_strerror (rc)); + err = gpg_error (GPG_ERR_GENERAL); + goto leave; + } + } +#else /*!HTTP_USE_GNUTLS && !HTTP_USE_NTBTLS*/ + { + (void)intended_hostname; + (void)flags; + } +#endif /*!HTTP_USE_GNUTLS && !HTTP_USE_NTBTLS*/ + + if (opt_debug > 1) + log_debug ("http.c:session_new: sess %p created\n", sess); + err = 0; + +#if USE_TLS + leave: +#endif /*USE_TLS*/ + if (err) + http_session_unref (sess); + else + *r_session = sess; + + return err; +} + + +/* Increment the reference count for session SESS. Passing NULL for + SESS is allowed. */ +http_session_t +http_session_ref (http_session_t sess) +{ + if (sess) + { + sess->refcount++; + if (opt_debug > 1) + log_debug ("http.c:session_ref: sess %p ref now %d\n", + sess, sess->refcount); + } + return sess; +} + + +void +http_session_set_log_cb (http_session_t sess, + void (*cb)(http_session_t, gpg_error_t, + const char *hostname, + const void **certs, size_t *certlens)) +{ + sess->cert_log_cb = cb; +} + + +/* Set the TIMEOUT in milliseconds for the connection's connect + * calls. Using 0 disables the timeout. */ +void +http_session_set_timeout (http_session_t sess, unsigned int timeout) +{ + sess->connect_timeout = timeout; +} + + + + +/* Start a HTTP retrieval and on success store at R_HD a context + pointer for completing the request and to wait for the response. + If HTTPHOST is not NULL it is used for the Host header instead of a + Host header derived from the URL. */ +gpg_error_t +http_open (http_t *r_hd, http_req_t reqtype, const char *url, + const char *httphost, + const char *auth, unsigned int flags, const char *proxy, + http_session_t session, const char *srvtag, strlist_t headers) +{ + gpg_error_t err; + http_t hd; + + *r_hd = NULL; + + if (!(reqtype == HTTP_REQ_GET || reqtype == HTTP_REQ_POST)) + return gpg_err_make (default_errsource, GPG_ERR_INV_ARG); + + /* Create the handle. */ + hd = xtrycalloc (1, sizeof *hd); + if (!hd) + return gpg_error_from_syserror (); + hd->magic = HTTP_CONTEXT_MAGIC; + hd->req_type = reqtype; + hd->flags = flags; + hd->session = http_session_ref (session); + + err = parse_uri (&hd->uri, url, 0, !!(flags & HTTP_FLAG_FORCE_TLS)); + if (!err) + err = send_request (hd, httphost, auth, proxy, srvtag, + hd->session? hd->session->connect_timeout : 0, + headers); + + if (err) + { + my_socket_unref (hd->sock, NULL, NULL); + if (hd->fp_read) + es_fclose (hd->fp_read); + if (hd->fp_write) + es_fclose (hd->fp_write); + http_session_unref (hd->session); + xfree (hd); + } + else + *r_hd = hd; + return err; +} + + +/* This function is useful to connect to a generic TCP service using + this http abstraction layer. This has the advantage of providing + service tags and an estream interface. TIMEOUT is in milliseconds. */ +gpg_error_t +http_raw_connect (http_t *r_hd, const char *server, unsigned short port, + unsigned int flags, const char *srvtag, unsigned int timeout) +{ + gpg_error_t err = 0; + http_t hd; + cookie_t cookie; + + *r_hd = NULL; + + if ((flags & HTTP_FLAG_FORCE_TOR)) + { + int mode; + + if (assuan_sock_get_flag (ASSUAN_INVALID_FD, "tor-mode", &mode) || !mode) + { + log_error ("Tor support is not available\n"); + return gpg_err_make (default_errsource, GPG_ERR_NOT_IMPLEMENTED); + } + /* Non-blocking connects do not work with our Tor proxy because + * we can't continue the Socks protocol after the EINPROGRESS. + * Disable the timeout to use a blocking connect. */ + timeout = 0; + } + + /* Create the handle. */ + hd = xtrycalloc (1, sizeof *hd); + if (!hd) + return gpg_error_from_syserror (); + hd->magic = HTTP_CONTEXT_MAGIC; + hd->req_type = HTTP_REQ_OPAQUE; + hd->flags = flags; + + /* Connect. */ + { + assuan_fd_t sock; + + err = connect_server (server, port, hd->flags, srvtag, timeout, &sock); + if (err) + { + xfree (hd); + return err; + } + hd->sock = my_socket_new (sock); + if (!hd->sock) + { + err = gpg_err_make (default_errsource, gpg_err_code_from_syserror ()); + xfree (hd); + return err; + } + } + + /* Setup estreams for reading and writing. */ + cookie = xtrycalloc (1, sizeof *cookie); + if (!cookie) + { + err = gpg_err_make (default_errsource, gpg_err_code_from_syserror ()); + goto leave; + } + cookie->sock = my_socket_ref (hd->sock); + hd->fp_write = es_fopencookie (cookie, "w", cookie_functions); + if (!hd->fp_write) + { + err = gpg_err_make (default_errsource, gpg_err_code_from_syserror ()); + my_socket_unref (cookie->sock, NULL, NULL); + xfree (cookie); + goto leave; + } + hd->write_cookie = cookie; /* Cookie now owned by FP_WRITE. */ + + cookie = xtrycalloc (1, sizeof *cookie); + if (!cookie) + { + err = gpg_err_make (default_errsource, gpg_err_code_from_syserror ()); + goto leave; + } + cookie->sock = my_socket_ref (hd->sock); + hd->fp_read = es_fopencookie (cookie, "r", cookie_functions); + if (!hd->fp_read) + { + err = gpg_err_make (default_errsource, gpg_err_code_from_syserror ()); + my_socket_unref (cookie->sock, NULL, NULL); + xfree (cookie); + goto leave; + } + hd->read_cookie = cookie; /* Cookie now owned by FP_READ. */ + + /* Register close notification to interlock the use of es_fclose in + http_close and in user code. */ + err = es_onclose (hd->fp_write, 1, fp_onclose_notification, hd); + if (!err) + err = es_onclose (hd->fp_read, 1, fp_onclose_notification, hd); + + leave: + if (err) + { + if (hd->fp_read) + es_fclose (hd->fp_read); + if (hd->fp_write) + es_fclose (hd->fp_write); + my_socket_unref (hd->sock, NULL, NULL); + xfree (hd); + } + else + *r_hd = hd; + return err; +} + + + + +void +http_start_data (http_t hd) +{ + if (!hd->in_data) + { + if (opt_debug || (hd->flags & HTTP_FLAG_LOG_RESP)) + log_debug_with_string ("\r\n", "http.c:request-header:"); + es_fputs ("\r\n", hd->fp_write); + es_fflush (hd->fp_write); + hd->in_data = 1; + } + else + es_fflush (hd->fp_write); +} + + +gpg_error_t +http_wait_response (http_t hd) +{ + gpg_error_t err; + cookie_t cookie; + int use_tls; + + /* Make sure that we are in the data. */ + http_start_data (hd); + + /* Close the write stream. Note that the reference counted socket + object keeps the actual system socket open. */ + cookie = hd->write_cookie; + if (!cookie) + return gpg_err_make (default_errsource, GPG_ERR_INTERNAL); + + use_tls = cookie->use_tls; + es_fclose (hd->fp_write); + hd->fp_write = NULL; + /* The close has released the cookie and thus we better set it to NULL. */ + hd->write_cookie = NULL; + + /* Shutdown one end of the socket is desired. As per HTTP/1.0 this + is not required but some very old servers (e.g. the original pksd + keyserver didn't worked without it. */ + if ((hd->flags & HTTP_FLAG_SHUTDOWN)) + shutdown (FD2INT (hd->sock->fd), 1); + hd->in_data = 0; + + /* Create a new cookie and a stream for reading. */ + cookie = xtrycalloc (1, sizeof *cookie); + if (!cookie) + return gpg_err_make (default_errsource, gpg_err_code_from_syserror ()); + cookie->sock = my_socket_ref (hd->sock); + cookie->session = http_session_ref (hd->session); + cookie->use_tls = use_tls; + + hd->read_cookie = cookie; + hd->fp_read = es_fopencookie (cookie, "r", cookie_functions); + if (!hd->fp_read) + { + err = gpg_err_make (default_errsource, gpg_err_code_from_syserror ()); + my_socket_unref (cookie->sock, NULL, NULL); + http_session_unref (cookie->session); + xfree (cookie); + hd->read_cookie = NULL; + return err; + } + + err = parse_response (hd); + + if (!err) + err = es_onclose (hd->fp_read, 1, fp_onclose_notification, hd); + + return err; +} + + +/* Convenience function to send a request and wait for the response. + Closes the handle on error. If PROXY is not NULL, this value will + be used as an HTTP proxy and any enabled $http_proxy gets + ignored. */ +gpg_error_t +http_open_document (http_t *r_hd, const char *document, + const char *auth, unsigned int flags, const char *proxy, + http_session_t session, + const char *srvtag, strlist_t headers) +{ + gpg_error_t err; + + err = http_open (r_hd, HTTP_REQ_GET, document, NULL, auth, flags, + proxy, session, srvtag, headers); + if (err) + return err; + + err = http_wait_response (*r_hd); + if (err) + http_close (*r_hd, 0); + + return err; +} + + +void +http_close (http_t hd, int keep_read_stream) +{ + if (!hd) + return; + + log_assert (hd->magic == HTTP_CONTEXT_MAGIC); + + /* First remove the close notifications for the streams. */ + if (hd->fp_read) + es_onclose (hd->fp_read, 0, fp_onclose_notification, hd); + if (hd->fp_write) + es_onclose (hd->fp_write, 0, fp_onclose_notification, hd); + + /* Now we can close the streams. */ + my_socket_unref (hd->sock, NULL, NULL); + if (hd->fp_read && !keep_read_stream) + es_fclose (hd->fp_read); + if (hd->fp_write) + es_fclose (hd->fp_write); + http_session_unref (hd->session); + hd->magic = 0xdeadbeef; + http_release_parsed_uri (hd->uri); + while (hd->headers) + { + header_t tmp = hd->headers->next; + xfree (hd->headers->value); + xfree (hd->headers); + hd->headers = tmp; + } + xfree (hd->buffer); + xfree (hd); +} + + +estream_t +http_get_read_ptr (http_t hd) +{ + return hd?hd->fp_read:NULL; +} + +estream_t +http_get_write_ptr (http_t hd) +{ + return hd?hd->fp_write:NULL; +} + +unsigned int +http_get_status_code (http_t hd) +{ + return hd?hd->status_code:0; +} + +/* Return information pertaining to TLS. If TLS is not in use for HD, + NULL is returned. WHAT is used ask for specific information: + + (NULL) := Only check whether TLS is in use. Returns an + unspecified string if TLS is in use. That string may + even be the empty string. + */ +const char * +http_get_tls_info (http_t hd, const char *what) +{ + (void)what; + + if (!hd) + return NULL; + + return hd->uri->use_tls? "":NULL; +} + + + +static gpg_error_t +parse_uri (parsed_uri_t *ret_uri, const char *uri, + int no_scheme_check, int force_tls) +{ + gpg_err_code_t ec; + + *ret_uri = xtrycalloc (1, sizeof **ret_uri + 2 * strlen (uri) + 1); + if (!*ret_uri) + return gpg_err_make (default_errsource, gpg_err_code_from_syserror ()); + strcpy ((*ret_uri)->buffer, uri); + strcpy ((*ret_uri)->buffer + strlen (uri) + 1, uri); + (*ret_uri)->original = (*ret_uri)->buffer + strlen (uri) + 1; + ec = do_parse_uri (*ret_uri, 0, no_scheme_check, force_tls); + if (ec) + { + http_release_parsed_uri (*ret_uri); + *ret_uri = NULL; + } + return gpg_err_make (default_errsource, ec); +} + + +/* + * Parse an URI and put the result into the newly allocated RET_URI. + * On success the caller must use http_release_parsed_uri() to + * releases the resources. If the HTTP_PARSE_NO_SCHEME_CHECK flag is + * set, the function tries to parse the URL in the same way it would + * do for an HTTP style URI. */ +gpg_error_t +http_parse_uri (parsed_uri_t *ret_uri, const char *uri, + unsigned int flags) +{ + return parse_uri (ret_uri, uri, !!(flags & HTTP_PARSE_NO_SCHEME_CHECK), 0); +} + + +void +http_release_parsed_uri (parsed_uri_t uri) +{ + if (uri) + { + uri_tuple_t r, r2; + + for (r = uri->params; r; r = r2) + { + r2 = r->next; + xfree (r); + } + for (r = uri->query; r; r = r2) + { + r2 = r->next; + xfree (r); + } + xfree (uri); + } +} + + +static gpg_err_code_t +do_parse_uri (parsed_uri_t uri, int only_local_part, + int no_scheme_check, int force_tls) +{ + uri_tuple_t *tail; + char *p, *p2, *p3, *pp; + int n; + + p = uri->buffer; + n = strlen (uri->buffer); + + /* Initialize all fields to an empty string or an empty list. */ + uri->scheme = uri->host = uri->path = p + n; + uri->port = 0; + uri->params = uri->query = NULL; + uri->use_tls = 0; + uri->is_http = 0; + uri->opaque = 0; + uri->v6lit = 0; + uri->onion = 0; + uri->explicit_port = 0; + uri->off_host = 0; + uri->off_path = 0; + + /* A quick validity check unless we have the opaque scheme. */ + if (strspn (p, VALID_URI_CHARS) != n + && strncmp (p, "opaque:", 7)) + return GPG_ERR_BAD_URI; /* Invalid characters found. */ + + if (!only_local_part) + { + /* Find the scheme. */ + if (!(p2 = strchr (p, ':')) || p2 == p) + return GPG_ERR_BAD_URI; /* No scheme. */ + *p2++ = 0; + for (pp=p; *pp; pp++) + *pp = tolower (*(unsigned char*)pp); + uri->scheme = p; + if (!strcmp (uri->scheme, "http") && !force_tls) + { + uri->port = 80; + uri->is_http = 1; + } + else if (!strcmp (uri->scheme, "hkp") && !force_tls) + { + uri->port = 11371; + uri->is_http = 1; + } +#ifdef USE_TLS + else if (!strcmp (uri->scheme, "https") || !strcmp (uri->scheme,"hkps") + || (force_tls && (!strcmp (uri->scheme, "http") + || !strcmp (uri->scheme,"hkp")))) + { + uri->port = 443; + uri->is_http = 1; + uri->use_tls = 1; + } +#endif /*USE_TLS*/ + else if (!strcmp (uri->scheme, "opaque")) + { + uri->opaque = 1; + uri->path = p2; + return 0; + } + else if (!no_scheme_check) + return GPG_ERR_INV_URI; /* Unsupported scheme */ + + p = p2; + + if (*p == '/' && p[1] == '/' ) /* There seems to be a hostname. */ + { + p += 2; + if ((p2 = strchr (p, '/'))) + { + if (p2 - uri->buffer > 10000) + return GPG_ERR_BAD_URI; + uri->off_path = p2 - uri->buffer; + *p2++ = 0; + } + else + { + n = (p - uri->buffer) + strlen (p); + if (n > 10000) + return GPG_ERR_BAD_URI; + uri->off_path = n; + } + + /* Check for username/password encoding */ + if ((p3 = strchr (p, '@'))) + { + uri->auth = p; + *p3++ = '\0'; + p = p3; + } + + for (pp=p; *pp; pp++) + *pp = tolower (*(unsigned char*)pp); + + /* Handle an IPv6 literal */ + if( *p == '[' && (p3=strchr( p, ']' )) ) + { + *p3++ = '\0'; + /* worst case, uri->host should have length 0, points to \0 */ + uri->host = p + 1; + if (p - uri->buffer > 10000) + return GPG_ERR_BAD_URI; + uri->off_host = (p + 1) - uri->buffer; + uri->v6lit = 1; + p = p3; + } + else + { + uri->host = p; + if (p - uri->buffer > 10000) + return GPG_ERR_BAD_URI; + uri->off_host = p - uri->buffer; + } + + if ((p3 = strchr (p, ':'))) + { + *p3++ = '\0'; + uri->port = atoi (p3); + uri->explicit_port = 1; + } + + if ((n = remove_escapes (uri->host)) < 0) + return GPG_ERR_BAD_URI; + if (n != strlen (uri->host)) + return GPG_ERR_BAD_URI; /* Hostname includes a Nul. */ + p = p2 ? p2 : NULL; + } + else if (uri->is_http) + return GPG_ERR_INV_URI; /* No Leading double slash for HTTP. */ + else + { + uri->opaque = 1; + uri->path = p; + if (is_onion_address (uri->path)) + uri->onion = 1; + return 0; + } + + } /* End global URI part. */ + + /* Parse the pathname part if any. */ + if (p && *p) + { + /* TODO: Here we have to check params. */ + + /* Do we have a query part? */ + if ((p2 = strchr (p, '?'))) + *p2++ = 0; + + uri->path = p; + if ((n = remove_escapes (p)) < 0) + return GPG_ERR_BAD_URI; + if (n != strlen (p)) + return GPG_ERR_BAD_URI; /* Path includes a Nul. */ + p = p2 ? p2 : NULL; + + /* Parse a query string if any. */ + if (p && *p) + { + tail = &uri->query; + for (;;) + { + uri_tuple_t elem; + + if ((p2 = strchr (p, '&'))) + *p2++ = 0; + if (!(elem = parse_tuple (p))) + return GPG_ERR_BAD_URI; + *tail = elem; + tail = &elem->next; + + if (!p2) + break; /* Ready. */ + p = p2; + } + } + } + + if (is_onion_address (uri->host)) + uri->onion = 1; + + return 0; +} + + +/* + * Remove all %xx escapes; this is done in-place. Returns: New length + * of the string. + */ +static int +remove_escapes (char *string) +{ + int n = 0; + unsigned char *p, *s; + + for (p = s = (unsigned char*)string; *s; s++) + { + if (*s == '%') + { + if (s[1] && s[2] && isxdigit (s[1]) && isxdigit (s[2])) + { + s++; + *p = *s >= '0' && *s <= '9' ? *s - '0' : + *s >= 'A' && *s <= 'F' ? *s - 'A' + 10 : *s - 'a' + 10; + *p <<= 4; + s++; + *p |= *s >= '0' && *s <= '9' ? *s - '0' : + *s >= 'A' && *s <= 'F' ? *s - 'A' + 10 : *s - 'a' + 10; + p++; + n++; + } + else + { + *p++ = *s++; + if (*s) + *p++ = *s++; + if (*s) + *p++ = *s++; + if (*s) + *p = 0; + return -1; /* Bad URI. */ + } + } + else + { + *p++ = *s; + n++; + } + } + *p = 0; /* Make sure to keep a string terminator. */ + return n; +} + + +/* If SPECIAL is NULL this function escapes in forms mode. */ +static size_t +escape_data (char *buffer, const void *data, size_t datalen, + const char *special) +{ + int forms = !special; + const unsigned char *s; + size_t n = 0; + + if (forms) + special = "%;?&="; + + for (s = data; datalen; s++, datalen--) + { + if (forms && *s == ' ') + { + if (buffer) + *buffer++ = '+'; + n++; + } + else if (forms && *s == '\n') + { + if (buffer) + memcpy (buffer, "%0D%0A", 6); + n += 6; + } + else if (forms && *s == '\r' && datalen > 1 && s[1] == '\n') + { + if (buffer) + memcpy (buffer, "%0D%0A", 6); + n += 6; + s++; + datalen--; + } + else if (strchr (VALID_URI_CHARS, *s) && !strchr (special, *s)) + { + if (buffer) + *(unsigned char*)buffer++ = *s; + n++; + } + else + { + if (buffer) + { + snprintf (buffer, 4, "%%%02X", *s); + buffer += 3; + } + n += 3; + } + } + return n; +} + + +static int +insert_escapes (char *buffer, const char *string, + const char *special) +{ + return escape_data (buffer, string, strlen (string), special); +} + + +/* Allocate a new string from STRING using standard HTTP escaping as + well as escaping of characters given in SPECIALS. A common pattern + for SPECIALS is "%;?&=". However it depends on the needs, for + example "+" and "/: often needs to be escaped too. Returns NULL on + failure and sets ERRNO. If SPECIAL is NULL a dedicated forms + encoding mode is used. */ +char * +http_escape_string (const char *string, const char *specials) +{ + int n; + char *buf; + + n = insert_escapes (NULL, string, specials); + buf = xtrymalloc (n+1); + if (buf) + { + insert_escapes (buf, string, specials); + buf[n] = 0; + } + return buf; +} + +/* Allocate a new string from {DATA,DATALEN} using standard HTTP + escaping as well as escaping of characters given in SPECIALS. A + common pattern for SPECIALS is "%;?&=". However it depends on the + needs, for example "+" and "/: often needs to be escaped too. + Returns NULL on failure and sets ERRNO. If SPECIAL is NULL a + dedicated forms encoding mode is used. */ +char * +http_escape_data (const void *data, size_t datalen, const char *specials) +{ + int n; + char *buf; + + n = escape_data (NULL, data, datalen, specials); + buf = xtrymalloc (n+1); + if (buf) + { + escape_data (buf, data, datalen, specials); + buf[n] = 0; + } + return buf; +} + + +static uri_tuple_t +parse_tuple (char *string) +{ + char *p = string; + char *p2; + int n; + uri_tuple_t tuple; + + if ((p2 = strchr (p, '='))) + *p2++ = 0; + if ((n = remove_escapes (p)) < 0) + return NULL; /* Bad URI. */ + if (n != strlen (p)) + return NULL; /* Name with a Nul in it. */ + tuple = xtrycalloc (1, sizeof *tuple); + if (!tuple) + return NULL; /* Out of core. */ + tuple->name = p; + if (!p2) /* We have only the name, so we assume an empty value string. */ + { + tuple->value = p + strlen (p); + tuple->valuelen = 0; + tuple->no_value = 1; /* Explicitly mark that we have seen no '='. */ + } + else /* Name and value. */ + { + if ((n = remove_escapes (p2)) < 0) + { + xfree (tuple); + return NULL; /* Bad URI. */ + } + tuple->value = p2; + tuple->valuelen = n; + } + return tuple; +} + + +/* Return true if STRING is likely "hostname:port" or only "hostname". */ +static int +is_hostname_port (const char *string) +{ + int colons = 0; + + if (!string || !*string) + return 0; + for (; *string; string++) + { + if (*string == ':') + { + if (colons) + return 0; + if (!string[1]) + return 0; + colons++; + } + else if (!colons && strchr (" \t\f\n\v_@[]/", *string)) + return 0; /* Invalid characters in hostname. */ + else if (colons && !digitp (string)) + return 0; /* Not a digit in the port. */ + } + return 1; +} + + +/* + * Send a HTTP request to the server + * Returns 0 if the request was successful + */ +static gpg_error_t +send_request (http_t hd, const char *httphost, const char *auth, + const char *proxy, const char *srvtag, unsigned int timeout, + strlist_t headers) +{ + gpg_error_t err; + const char *server; + char *request, *p; + unsigned short port; + const char *http_proxy = NULL; + char *proxy_authstr = NULL; + char *authstr = NULL; + assuan_fd_t sock; +#ifdef USE_TLS + int have_http_proxy = 0; +#endif + + if (hd->uri->use_tls && !hd->session) + { + log_error ("TLS requested but no session object provided\n"); + return gpg_err_make (default_errsource, GPG_ERR_INTERNAL); + } +#ifdef USE_TLS + if (hd->uri->use_tls && !hd->session->tls_session) + { + log_error ("TLS requested but no TLS context available\n"); + return gpg_err_make (default_errsource, GPG_ERR_INTERNAL); + } + if (opt_debug) + log_debug ("Using TLS library: %s %s\n", +# if HTTP_USE_NTBTLS + "NTBTLS", ntbtls_check_version (NULL) +# elif HTTP_USE_GNUTLS + "GNUTLS", gnutls_check_version (NULL) +# else + "?", "?" +# endif /*HTTP_USE_*TLS*/ + ); +#endif /*USE_TLS*/ + + if ((hd->flags & HTTP_FLAG_FORCE_TOR)) + { + int mode; + + if (assuan_sock_get_flag (ASSUAN_INVALID_FD, "tor-mode", &mode) || !mode) + { + log_error ("Tor support is not available\n"); + return gpg_err_make (default_errsource, GPG_ERR_NOT_IMPLEMENTED); + } + /* Non-blocking connects do not work with our Tor proxy because + * we can't continue the Socks protocol after the EINPROGRESS. + * Disable the timeout to use a blocking connect. */ + timeout = 0; + } + + server = *hd->uri->host ? hd->uri->host : "localhost"; + port = hd->uri->port ? hd->uri->port : 80; + + /* Try to use SNI. */ +#ifdef USE_TLS + if (hd->uri->use_tls) + { +# if HTTP_USE_GNUTLS + int rc; +# endif + + xfree (hd->session->servername); + hd->session->servername = xtrystrdup (httphost? httphost : server); + if (!hd->session->servername) + { + err = gpg_err_make (default_errsource, gpg_err_code_from_syserror ()); + return err; + } + +# if HTTP_USE_NTBTLS + err = ntbtls_set_hostname (hd->session->tls_session, + hd->session->servername); + if (err) + { + log_info ("ntbtls_set_hostname failed: %s\n", gpg_strerror (err)); + return err; + } +# elif HTTP_USE_GNUTLS + rc = gnutls_server_name_set (hd->session->tls_session, + GNUTLS_NAME_DNS, + hd->session->servername, + strlen (hd->session->servername)); + if (rc < 0) + log_info ("gnutls_server_name_set failed: %s\n", gnutls_strerror (rc)); +# endif /*HTTP_USE_GNUTLS*/ + } +#endif /*USE_TLS*/ + + if ( (proxy && *proxy) + || ( (hd->flags & HTTP_FLAG_TRY_PROXY) + && (http_proxy = getenv (HTTP_PROXY_ENV)) + && *http_proxy )) + { + parsed_uri_t uri; + + if (proxy) + http_proxy = proxy; + + err = parse_uri (&uri, http_proxy, 0, 0); + if (gpg_err_code (err) == GPG_ERR_INV_URI + && is_hostname_port (http_proxy)) + { + /* Retry assuming a "hostname:port" string. */ + char *tmpname = strconcat ("http://", http_proxy, NULL); + if (tmpname && !parse_uri (&uri, tmpname, 0, 0)) + err = 0; + xfree (tmpname); + } + + if (err) + ; +#ifdef USE_TLS + else if (!strcmp (uri->scheme, "http")) + have_http_proxy = 1; +#endif + else if (!strcmp (uri->scheme, "socks4") + || !strcmp (uri->scheme, "socks5h")) + err = gpg_err_make (default_errsource, GPG_ERR_NOT_IMPLEMENTED); + else + err = gpg_err_make (default_errsource, GPG_ERR_INV_URI); + + if (err) + { + log_error ("invalid HTTP proxy (%s): %s\n", + http_proxy, gpg_strerror (err)); + return gpg_err_make (default_errsource, GPG_ERR_CONFIGURATION); + } + + if (uri->auth) + { + remove_escapes (uri->auth); + proxy_authstr = make_header_line ("Proxy-Authorization: Basic ", + "\r\n", + uri->auth, strlen(uri->auth)); + if (!proxy_authstr) + { + err = gpg_err_make (default_errsource, + gpg_err_code_from_syserror ()); + http_release_parsed_uri (uri); + return err; + } + } + + err = connect_server (*uri->host ? uri->host : "localhost", + uri->port ? uri->port : 80, + hd->flags, NULL, timeout, &sock); + http_release_parsed_uri (uri); + } + else + { + err = connect_server (server, port, hd->flags, srvtag, timeout, &sock); + } + + if (err) + { + xfree (proxy_authstr); + return err; + } + hd->sock = my_socket_new (sock); + if (!hd->sock) + { + xfree (proxy_authstr); + return gpg_err_make (default_errsource, gpg_err_code_from_syserror ()); + } + +#if USE_TLS + if (have_http_proxy && hd->uri->use_tls) + { + int saved_flags; + cookie_t cookie; + + /* Try to use the CONNECT method to proxy our TLS stream. */ + request = es_bsprintf + ("CONNECT %s:%hu HTTP/1.0\r\nHost: %s:%hu\r\n%s", + httphost ? httphost : server, + port, + httphost ? httphost : server, + port, + proxy_authstr ? proxy_authstr : ""); + xfree (proxy_authstr); + proxy_authstr = NULL; + + if (! request) + return gpg_err_make (default_errsource, gpg_err_code_from_syserror ()); + + if (opt_debug || (hd->flags & HTTP_FLAG_LOG_RESP)) + log_debug_with_string (request, "http.c:request:"); + + cookie = xtrycalloc (1, sizeof *cookie); + if (! cookie) + { + err = gpg_err_make (default_errsource, gpg_err_code_from_syserror ()); + xfree (request); + return err; + } + cookie->sock = my_socket_ref (hd->sock); + hd->write_cookie = cookie; + + hd->fp_write = es_fopencookie (cookie, "w", cookie_functions); + if (! hd->fp_write) + { + err = gpg_err_make (default_errsource, gpg_err_code_from_syserror ()); + my_socket_unref (cookie->sock, NULL, NULL); + xfree (cookie); + xfree (request); + hd->write_cookie = NULL; + return err; + } + else if (es_fputs (request, hd->fp_write) || es_fflush (hd->fp_write)) + err = gpg_err_make (default_errsource, gpg_err_code_from_syserror ()); + + xfree (request); + request = NULL; + + /* Make sure http_wait_response doesn't close the stream. */ + saved_flags = hd->flags; + hd->flags &= ~HTTP_FLAG_SHUTDOWN; + + /* Get the response. */ + err = http_wait_response (hd); + + /* Restore flags, destroy stream. */ + hd->flags = saved_flags; + es_fclose (hd->fp_read); + hd->fp_read = NULL; + hd->read_cookie = NULL; + + /* Reset state. */ + hd->in_data = 0; + + if (err) + return err; + + if (hd->status_code != 200) + { + request = es_bsprintf + ("CONNECT %s:%hu", + httphost ? httphost : server, + port); + + log_error (_("error accessing '%s': http status %u\n"), + request ? request : "out of core", + http_get_status_code (hd)); + + xfree (request); + return gpg_error (GPG_ERR_NO_DATA); + } + + /* We are done with the proxy, the code below will establish a + * TLS session and talk directly to the target server. */ + http_proxy = NULL; + } +#endif /* USE_TLS */ + +#if HTTP_USE_NTBTLS + if (hd->uri->use_tls) + { + estream_t in, out; + + my_socket_ref (hd->sock); + + /* Until we support send/recv in estream under Windows we need + * to use es_fopencookie. */ +#ifdef HAVE_W32_SYSTEM + in = es_fopencookie ((void*)(unsigned int)hd->sock->fd, "rb", + simple_cookie_functions); +#else + in = es_fdopen_nc (hd->sock->fd, "rb"); +#endif + if (!in) + { + err = gpg_error_from_syserror (); + xfree (proxy_authstr); + return err; + } + +#ifdef HAVE_W32_SYSTEM + out = es_fopencookie ((void*)(unsigned int)hd->sock->fd, "wb", + simple_cookie_functions); +#else + out = es_fdopen_nc (hd->sock->fd, "wb"); +#endif + if (!out) + { + err = gpg_error_from_syserror (); + es_fclose (in); + xfree (proxy_authstr); + return err; + } + + err = ntbtls_set_transport (hd->session->tls_session, in, out); + if (err) + { + log_info ("TLS set_transport failed: %s <%s>\n", + gpg_strerror (err), gpg_strsource (err)); + xfree (proxy_authstr); + return err; + } + +#ifdef HTTP_USE_NTBTLS + if (hd->session->verify_cb) + { + err = ntbtls_set_verify_cb (hd->session->tls_session, + my_ntbtls_verify_cb, hd); + if (err) + { + log_error ("ntbtls_set_verify_cb failed: %s\n", + gpg_strerror (err)); + xfree (proxy_authstr); + return err; + } + } +#endif /*HTTP_USE_NTBTLS*/ + + while ((err = ntbtls_handshake (hd->session->tls_session))) + { +#if NTBTLS_VERSION_NUMBER >= 0x000200 + unsigned int tlevel, ttype; + const char *s = ntbtls_get_last_alert (hd->session->tls_session, + &tlevel, &ttype); + if (s) + log_info ("TLS alert: %s (%u.%u)\n", s, tlevel, ttype); +#endif + + switch (err) + { + default: + log_info ("TLS handshake failed: %s <%s>\n", + gpg_strerror (err), gpg_strsource (err)); + xfree (proxy_authstr); + return err; + } + } + + hd->session->verify.done = 0; + + /* Try the available verify callbacks until one returns success + * or a real error. Note that NTBTLS does the verification + * during the handshake via */ +#ifdef HTTP_USE_NTBTLS + err = 0; /* Fixme check that the CB has been called. */ +#else + err = gpg_error (GPG_ERR_NOT_IMPLEMENTED); +#endif + + if (hd->session->verify_cb + && gpg_err_source (err) == GPG_ERR_SOURCE_DIRMNGR + && gpg_err_code (err) == GPG_ERR_NOT_IMPLEMENTED) + err = hd->session->verify_cb (hd->session->verify_cb_value, + hd, hd->session, + (hd->flags | hd->session->flags), + hd->session->tls_session); + + if (tls_callback + && gpg_err_source (err) == GPG_ERR_SOURCE_DIRMNGR + && gpg_err_code (err) == GPG_ERR_NOT_IMPLEMENTED) + err = tls_callback (hd, hd->session, 0); + + if (gpg_err_source (err) == GPG_ERR_SOURCE_DIRMNGR + && gpg_err_code (err) == GPG_ERR_NOT_IMPLEMENTED) + err = http_verify_server_credentials (hd->session); + + if (err) + { + log_info ("TLS connection authentication failed: %s <%s>\n", + gpg_strerror (err), gpg_strsource (err)); + xfree (proxy_authstr); + return err; + } + + } +#elif HTTP_USE_GNUTLS + if (hd->uri->use_tls) + { + int rc; + + my_socket_ref (hd->sock); + gnutls_transport_set_ptr (hd->session->tls_session, hd->sock); + gnutls_transport_set_pull_function (hd->session->tls_session, + my_gnutls_read); + gnutls_transport_set_push_function (hd->session->tls_session, + my_gnutls_write); + + handshake_again: + do + { + rc = gnutls_handshake (hd->session->tls_session); + } + while (rc == GNUTLS_E_INTERRUPTED || rc == GNUTLS_E_AGAIN); + if (rc < 0) + { + if (rc == GNUTLS_E_WARNING_ALERT_RECEIVED + || rc == GNUTLS_E_FATAL_ALERT_RECEIVED) + { + gnutls_alert_description_t alertno; + const char *alertstr; + + alertno = gnutls_alert_get (hd->session->tls_session); + alertstr = gnutls_alert_get_name (alertno); + log_info ("TLS handshake %s: %s (alert %d)\n", + rc == GNUTLS_E_WARNING_ALERT_RECEIVED + ? "warning" : "failed", + alertstr, (int)alertno); + if (alertno == GNUTLS_A_UNRECOGNIZED_NAME && server) + log_info (" (sent server name '%s')\n", server); + + if (rc == GNUTLS_E_WARNING_ALERT_RECEIVED) + goto handshake_again; + } + else + log_info ("TLS handshake failed: %s\n", gnutls_strerror (rc)); + xfree (proxy_authstr); + return gpg_err_make (default_errsource, GPG_ERR_NETWORK); + } + + hd->session->verify.done = 0; + if (tls_callback) + err = tls_callback (hd, hd->session, 0); + else + err = http_verify_server_credentials (hd->session); + if (err) + { + log_info ("TLS connection authentication failed: %s\n", + gpg_strerror (err)); + xfree (proxy_authstr); + return err; + } + } +#endif /*HTTP_USE_GNUTLS*/ + + if (auth || hd->uri->auth) + { + char *myauth; + + if (auth) + { + myauth = xtrystrdup (auth); + if (!myauth) + { + xfree (proxy_authstr); + return gpg_err_make (default_errsource, gpg_err_code_from_syserror ()); + } + remove_escapes (myauth); + } + else + { + remove_escapes (hd->uri->auth); + myauth = hd->uri->auth; + } + + authstr = make_header_line ("Authorization: Basic ", "\r\n", + myauth, strlen (myauth)); + if (auth) + xfree (myauth); + + if (!authstr) + { + xfree (proxy_authstr); + return gpg_err_make (default_errsource, + gpg_err_code_from_syserror ()); + } + } + + p = build_rel_path (hd->uri); + if (!p) + return gpg_err_make (default_errsource, gpg_err_code_from_syserror ()); + + if (http_proxy && *http_proxy) + { + request = es_bsprintf + ("%s %s://%s:%hu%s%s HTTP/1.0\r\n%s%s", + hd->req_type == HTTP_REQ_GET ? "GET" : + hd->req_type == HTTP_REQ_HEAD ? "HEAD" : + hd->req_type == HTTP_REQ_POST ? "POST" : "OOPS", + hd->uri->use_tls? "https" : "http", + httphost? httphost : server, + port, *p == '/' ? "" : "/", p, + authstr ? authstr : "", + proxy_authstr ? proxy_authstr : ""); + } + else + { + char portstr[35]; + + if (port == (hd->uri->use_tls? 443 : 80)) + *portstr = 0; + else + snprintf (portstr, sizeof portstr, ":%u", port); + + request = es_bsprintf + ("%s %s%s HTTP/1.0\r\nHost: %s%s\r\n%s", + hd->req_type == HTTP_REQ_GET ? "GET" : + hd->req_type == HTTP_REQ_HEAD ? "HEAD" : + hd->req_type == HTTP_REQ_POST ? "POST" : "OOPS", + *p == '/' ? "" : "/", p, + httphost? httphost : server, + portstr, + authstr? authstr:""); + } + xfree (p); + if (!request) + { + err = gpg_err_make (default_errsource, gpg_err_code_from_syserror ()); + xfree (authstr); + xfree (proxy_authstr); + return err; + } + + if (opt_debug || (hd->flags & HTTP_FLAG_LOG_RESP)) + log_debug_with_string (request, "http.c:request:"); + + /* First setup estream so that we can write even the first line + using estream. This is also required for the sake of gnutls. */ + { + cookie_t cookie; + + cookie = xtrycalloc (1, sizeof *cookie); + if (!cookie) + { + err = gpg_err_make (default_errsource, gpg_err_code_from_syserror ()); + goto leave; + } + cookie->sock = my_socket_ref (hd->sock); + hd->write_cookie = cookie; + cookie->use_tls = hd->uri->use_tls; + cookie->session = http_session_ref (hd->session); + + hd->fp_write = es_fopencookie (cookie, "w", cookie_functions); + if (!hd->fp_write) + { + err = gpg_err_make (default_errsource, gpg_err_code_from_syserror ()); + my_socket_unref (cookie->sock, NULL, NULL); + xfree (cookie); + hd->write_cookie = NULL; + } + else if (es_fputs (request, hd->fp_write) || es_fflush (hd->fp_write)) + err = gpg_err_make (default_errsource, gpg_err_code_from_syserror ()); + else + err = 0; + + if (!err) + { + for (;headers; headers=headers->next) + { + if (opt_debug || (hd->flags & HTTP_FLAG_LOG_RESP)) + log_debug_with_string (headers->d, "http.c:request-header:"); + if ((es_fputs (headers->d, hd->fp_write) || es_fflush (hd->fp_write)) + || (es_fputs("\r\n",hd->fp_write) || es_fflush(hd->fp_write))) + { + err = gpg_err_make (default_errsource, + gpg_err_code_from_syserror ()); + break; + } + } + } + } + + leave: + es_free (request); + xfree (authstr); + xfree (proxy_authstr); + + return err; +} + + +/* + * Build the relative path from the parsed URI. Minimal + * implementation. May return NULL in case of memory failure; errno + * is then set accordingly. + */ +static char * +build_rel_path (parsed_uri_t uri) +{ + uri_tuple_t r; + char *rel_path, *p; + int n; + + /* Count the needed space. */ + n = insert_escapes (NULL, uri->path, "%;?&"); + /* TODO: build params. */ + for (r = uri->query; r; r = r->next) + { + n++; /* '?'/'&' */ + n += insert_escapes (NULL, r->name, "%;?&="); + if (!r->no_value) + { + n++; /* '=' */ + n += insert_escapes (NULL, r->value, "%;?&="); + } + } + n++; + + /* Now allocate and copy. */ + p = rel_path = xtrymalloc (n); + if (!p) + return NULL; + n = insert_escapes (p, uri->path, "%;?&"); + p += n; + /* TODO: add params. */ + for (r = uri->query; r; r = r->next) + { + *p++ = r == uri->query ? '?' : '&'; + n = insert_escapes (p, r->name, "%;?&="); + p += n; + if (!r->no_value) + { + *p++ = '='; + /* TODO: Use valuelen. */ + n = insert_escapes (p, r->value, "%;?&="); + p += n; + } + } + *p = 0; + return rel_path; +} + + +/* Transform a header name into a standard capitalized format; e.g. + "Content-Type". Conversion stops at the colon. As usual we don't + use the localized versions of ctype.h. */ +static void +capitalize_header_name (char *name) +{ + int first = 1; + + for (; *name && *name != ':'; name++) + { + if (*name == '-') + first = 1; + else if (first) + { + if (*name >= 'a' && *name <= 'z') + *name = *name - 'a' + 'A'; + first = 0; + } + else if (*name >= 'A' && *name <= 'Z') + *name = *name - 'A' + 'a'; + } +} + + +/* Store an HTTP header line in LINE away. Line continuation is + supported as well as merging of headers with the same name. This + function may modify LINE. */ +static gpg_err_code_t +store_header (http_t hd, char *line) +{ + size_t n; + char *p, *value; + header_t h; + + n = strlen (line); + if (n && line[n-1] == '\n') + { + line[--n] = 0; + if (n && line[n-1] == '\r') + line[--n] = 0; + } + if (!n) /* we are never called to hit this. */ + return GPG_ERR_BUG; + if (*line == ' ' || *line == '\t') + { + /* Continuation. This won't happen too often as it is not + recommended. We use a straightforward implementation. */ + if (!hd->headers) + return GPG_ERR_PROTOCOL_VIOLATION; + n += strlen (hd->headers->value); + p = xtrymalloc (n+1); + if (!p) + return gpg_err_code_from_syserror (); + strcpy (stpcpy (p, hd->headers->value), line); + xfree (hd->headers->value); + hd->headers->value = p; + return 0; + } + + capitalize_header_name (line); + p = strchr (line, ':'); + if (!p) + return GPG_ERR_PROTOCOL_VIOLATION; + *p++ = 0; + while (*p == ' ' || *p == '\t') + p++; + value = p; + + for (h=hd->headers; h; h = h->next) + if ( !strcmp (h->name, line) ) + break; + if (h) + { + /* We have already seen a line with that name. Thus we assume + * it is a comma separated list and merge them. */ + p = strconcat (h->value, ",", value, NULL); + if (!p) + return gpg_err_code_from_syserror (); + xfree (h->value); + h->value = p; + return 0; + } + + /* Append a new header. */ + h = xtrymalloc (sizeof *h + strlen (line)); + if (!h) + return gpg_err_code_from_syserror (); + strcpy (h->name, line); + h->value = xtrymalloc (strlen (value)+1); + if (!h->value) + { + xfree (h); + return gpg_err_code_from_syserror (); + } + strcpy (h->value, value); + h->next = hd->headers; + hd->headers = h; + + return 0; +} + + +/* Return the header NAME from the last response. The returned value + is valid as along as HD has not been closed and no other request + has been send. If the header was not found, NULL is returned. NAME + must be canonicalized, that is the first letter of each dash + delimited part must be uppercase and all other letters lowercase. */ +const char * +http_get_header (http_t hd, const char *name) +{ + header_t h; + + for (h=hd->headers; h; h = h->next) + if ( !strcmp (h->name, name) ) + return h->value; + return NULL; +} + + +/* Return a newly allocated and NULL terminated array with pointers to + header names. The array must be released with xfree() and its + content is only values as long as no other request has been + send. */ +const char ** +http_get_header_names (http_t hd) +{ + const char **array; + size_t n; + header_t h; + + for (n=0, h = hd->headers; h; h = h->next) + n++; + array = xtrycalloc (n+1, sizeof *array); + if (array) + { + for (n=0, h = hd->headers; h; h = h->next) + array[n++] = h->name; + } + + return array; +} + + +/* + * Parse the response from a server. + * Returns: Errorcode and sets some files in the handle + */ +static gpg_err_code_t +parse_response (http_t hd) +{ + char *line, *p, *p2; + size_t maxlen, len; + cookie_t cookie = hd->read_cookie; + const char *s; + + /* Delete old header lines. */ + while (hd->headers) + { + header_t tmp = hd->headers->next; + xfree (hd->headers->value); + xfree (hd->headers); + hd->headers = tmp; + } + + /* Wait for the status line. */ + do + { + maxlen = MAX_LINELEN; + len = es_read_line (hd->fp_read, &hd->buffer, &hd->buffer_size, &maxlen); + line = hd->buffer; + if (!line) + return gpg_err_code_from_syserror (); /* Out of core. */ + if (!maxlen) + return GPG_ERR_TRUNCATED; /* Line has been truncated. */ + if (!len) + return GPG_ERR_EOF; + + if (opt_debug || (hd->flags & HTTP_FLAG_LOG_RESP)) + log_debug_with_string (line, "http.c:response:\n"); + } + while (!*line); + + if ((p = strchr (line, '/'))) + *p++ = 0; + if (!p || strcmp (line, "HTTP")) + return 0; /* Assume http 0.9. */ + + if ((p2 = strpbrk (p, " \t"))) + { + *p2++ = 0; + p2 += strspn (p2, " \t"); + } + if (!p2) + return 0; /* Also assume http 0.9. */ + p = p2; + /* TODO: Add HTTP version number check. */ + if ((p2 = strpbrk (p, " \t"))) + *p2++ = 0; + if (!isdigit ((unsigned int)p[0]) || !isdigit ((unsigned int)p[1]) + || !isdigit ((unsigned int)p[2]) || p[3]) + { + /* Malformed HTTP status code - assume http 0.9. */ + hd->is_http_0_9 = 1; + hd->status_code = 200; + return 0; + } + hd->status_code = atoi (p); + + /* Skip all the header lines and wait for the empty line. */ + do + { + maxlen = MAX_LINELEN; + len = es_read_line (hd->fp_read, &hd->buffer, &hd->buffer_size, &maxlen); + line = hd->buffer; + if (!line) + return gpg_err_code_from_syserror (); /* Out of core. */ + /* Note, that we can silently ignore truncated lines. */ + if (!len) + return GPG_ERR_EOF; + /* Trim line endings of empty lines. */ + if ((*line == '\r' && line[1] == '\n') || *line == '\n') + *line = 0; + if (opt_debug || (hd->flags & HTTP_FLAG_LOG_RESP)) + log_info ("http.c:RESP: '%.*s'\n", + (int)strlen(line)-(*line&&line[1]?2:0),line); + if (*line) + { + gpg_err_code_t ec = store_header (hd, line); + if (ec) + return ec; + } + } + while (len && *line); + + cookie->content_length_valid = 0; + if (!(hd->flags & HTTP_FLAG_IGNORE_CL)) + { + s = http_get_header (hd, "Content-Length"); + if (s) + { + cookie->content_length_valid = 1; + cookie->content_length = string_to_u64 (s); + } + } + + return 0; +} + +#if 0 +static int +start_server () +{ + struct sockaddr_in mya; + struct sockaddr_in peer; + int fd, client; + fd_set rfds; + int addrlen; + int i; + + if ((fd = socket (AF_INET, SOCK_STREAM, 0)) == -1) + { + log_error ("socket() failed: %s\n", strerror (errno)); + return -1; + } + i = 1; + if (setsockopt (fd, SOL_SOCKET, SO_REUSEADDR, (byte *) & i, sizeof (i))) + log_info ("setsockopt(SO_REUSEADDR) failed: %s\n", strerror (errno)); + + mya.sin_family = AF_INET; + memset (&mya.sin_addr, 0, sizeof (mya.sin_addr)); + mya.sin_port = htons (11371); + + if (bind (fd, (struct sockaddr *) &mya, sizeof (mya))) + { + log_error ("bind to port 11371 failed: %s\n", strerror (errno)); + sock_close (fd); + return -1; + } + + if (listen (fd, 5)) + { + log_error ("listen failed: %s\n", strerror (errno)); + sock_close (fd); + return -1; + } + + for (;;) + { + FD_ZERO (&rfds); + FD_SET (fd, &rfds); + + if (my_select (fd + 1, &rfds, NULL, NULL, NULL) <= 0) + continue; /* ignore any errors */ + + if (!FD_ISSET (fd, &rfds)) + continue; + + addrlen = sizeof peer; + client = my_accept (fd, (struct sockaddr *) &peer, &addrlen); + if (client == -1) + continue; /* oops */ + + log_info ("connect from %s\n", inet_ntoa (peer.sin_addr)); + + fflush (stdout); + fflush (stderr); + if (!fork ()) + { + int c; + FILE *fp; + + fp = fdopen (client, "r"); + while ((c = getc (fp)) != EOF) + putchar (c); + fclose (fp); + exit (0); + } + sock_close (client); + } + + + return 0; +} +#endif + + + +/* Return true if SOCKS shall be used. This is the case if tor_mode + * is enabled and the desired address is not the loopback address. + * This function is basically a copy of the same internal function in + * Libassuan. */ +static int +use_socks (struct sockaddr_storage *addr) +{ + int mode; + + if (assuan_sock_get_flag (ASSUAN_INVALID_FD, "tor-mode", &mode) || !mode) + return 0; /* Not in Tor mode. */ + else if (addr->ss_family == AF_INET6) + { + struct sockaddr_in6 *addr_in6 = (struct sockaddr_in6 *)addr; + const unsigned char *s; + int i; + + s = (unsigned char *)&addr_in6->sin6_addr.s6_addr; + if (s[15] != 1) + return 1; /* Last octet is not 1 - not the loopback address. */ + for (i=0; i < 15; i++, s++) + if (*s) + return 1; /* Non-zero octet found - not the loopback address. */ + + return 0; /* This is the loopback address. */ + } + else if (addr->ss_family == AF_INET) + { + struct sockaddr_in *addr_in = (struct sockaddr_in *)addr; + + if (*(unsigned char*)&addr_in->sin_addr.s_addr == 127) + return 0; /* Loopback (127.0.0.0/8) */ + + return 1; + } + else + return 0; +} + + +/* Wrapper around assuan_sock_new which takes the domain from an + * address parameter. */ +static assuan_fd_t +my_sock_new_for_addr (struct sockaddr_storage *addr, int type, int proto) +{ + int domain; + + if (use_socks (addr)) + { + /* Libassaun always uses 127.0.0.1 to connect to the socks + * server (i.e. the Tor daemon). */ + domain = AF_INET; + } + else + domain = addr->ss_family; + + return assuan_sock_new (domain, type, proto); +} + + +/* Call WSAGetLastError and map it to a libgpg-error. */ +#ifdef HAVE_W32_SYSTEM +static gpg_error_t +my_wsagetlasterror (void) +{ + int wsaerr; + gpg_err_code_t ec; + + wsaerr = WSAGetLastError (); + switch (wsaerr) + { + case WSAENOTSOCK: ec = GPG_ERR_EINVAL; break; + case WSAEWOULDBLOCK: ec = GPG_ERR_EAGAIN; break; + case ERROR_BROKEN_PIPE: ec = GPG_ERR_EPIPE; break; + case WSANOTINITIALISED: ec = GPG_ERR_ENOSYS; break; + case WSAENOBUFS: ec = GPG_ERR_ENOBUFS; break; + case WSAEMSGSIZE: ec = GPG_ERR_EMSGSIZE; break; + case WSAECONNREFUSED: ec = GPG_ERR_ECONNREFUSED; break; + case WSAEISCONN: ec = GPG_ERR_EISCONN; break; + case WSAEALREADY: ec = GPG_ERR_EALREADY; break; + case WSAETIMEDOUT: ec = GPG_ERR_ETIMEDOUT; break; + default: ec = GPG_ERR_EIO; break; + } + + return gpg_err_make (default_errsource, ec); +} +#endif /*HAVE_W32_SYSTEM*/ + + +/* Connect SOCK and return GPG_ERR_ETIMEOUT if a connection could not + * be established within TIMEOUT milliseconds. 0 indicates the + * system's default timeout. The other args are the usual connect + * args. On success 0 is returned, on timeout GPG_ERR_ETIMEDOUT, and + * another error code for other errors. On timeout the caller needs + * to close the socket as soon as possible to stop an ongoing + * handshake. + * + * This implementation is for well-behaving systems; see Stevens, + * Network Programming, 2nd edition, Vol 1, 15.4. */ +static gpg_error_t +connect_with_timeout (assuan_fd_t sock, + struct sockaddr *addr, int addrlen, + unsigned int timeout) +{ + gpg_error_t err; + int syserr; + socklen_t slen; + fd_set rset, wset; + struct timeval tval; + int n; + +#ifndef HAVE_W32_SYSTEM + int oflags; +# define RESTORE_BLOCKING() do { \ + fcntl (sock, F_SETFL, oflags); \ + } while (0) +#else /*HAVE_W32_SYSTEM*/ +# define RESTORE_BLOCKING() do { \ + unsigned long along = 0; \ + ioctlsocket (FD2INT (sock), FIONBIO, &along); \ + } while (0) +#endif /*HAVE_W32_SYSTEM*/ + + + if (!timeout) + { + /* Shortcut. */ + if (assuan_sock_connect (sock, addr, addrlen)) + err = gpg_err_make (default_errsource, gpg_err_code_from_syserror ()); + else + err = 0; + return err; + } + + /* Switch the socket into non-blocking mode. */ +#ifdef HAVE_W32_SYSTEM + { + unsigned long along = 1; + if (ioctlsocket (FD2INT (sock), FIONBIO, &along)) + return my_wsagetlasterror (); + } +#else + oflags = fcntl (sock, F_GETFL, 0); + if (fcntl (sock, F_SETFL, oflags | O_NONBLOCK)) + return gpg_err_make (default_errsource, gpg_err_code_from_syserror ()); +#endif + + /* Do the connect. */ + if (!assuan_sock_connect (sock, addr, addrlen)) + { + /* Immediate connect. Restore flags. */ + RESTORE_BLOCKING (); + return 0; /* Success. */ + } + err = gpg_err_make (default_errsource, gpg_err_code_from_syserror ()); + if (gpg_err_code (err) != GPG_ERR_EINPROGRESS +#ifdef HAVE_W32_SYSTEM + && gpg_err_code (err) != GPG_ERR_EAGAIN +#endif + ) + { + RESTORE_BLOCKING (); + return err; + } + + FD_ZERO (&rset); + FD_SET (FD2INT (sock), &rset); + wset = rset; + tval.tv_sec = timeout / 1000; + tval.tv_usec = (timeout % 1000) * 1000; + + n = my_select (FD2INT(sock)+1, &rset, &wset, NULL, &tval); + if (n < 0) + { + err = gpg_err_make (default_errsource, gpg_err_code_from_syserror ()); + RESTORE_BLOCKING (); + return err; + } + if (!n) + { + /* Timeout: We do not restore the socket flags on timeout + * because the caller is expected to close the socket. */ + return gpg_err_make (default_errsource, GPG_ERR_ETIMEDOUT); + } + if (!FD_ISSET (sock, &rset) && !FD_ISSET (sock, &wset)) + { + /* select misbehaved. */ + return gpg_err_make (default_errsource, GPG_ERR_SYSTEM_BUG); + } + + slen = sizeof (syserr); + if (getsockopt (FD2INT(sock), SOL_SOCKET, SO_ERROR, + (void*)&syserr, &slen) < 0) + { + /* Assume that this is Solaris which returns the error in ERRNO. */ + err = gpg_err_make (default_errsource, gpg_err_code_from_syserror ()); + } + else if (syserr) + err = gpg_err_make (default_errsource, gpg_err_code_from_errno (syserr)); + else + err = 0; /* Connected. */ + + RESTORE_BLOCKING (); + + return err; + +#undef RESTORE_BLOCKING +} + + +/* Actually connect to a server. On success 0 is returned and the + * file descriptor for the socket is stored at R_SOCK; on error an + * error code is returned and ASSUAN_INVALID_FD is stored at R_SOCK. + * TIMEOUT is the connect timeout in milliseconds. Note that the + * function tries to connect to all known addresses and the timeout is + * for each one. */ +static gpg_error_t +connect_server (const char *server, unsigned short port, + unsigned int flags, const char *srvtag, unsigned int timeout, + assuan_fd_t *r_sock) +{ + gpg_error_t err; + assuan_fd_t sock = ASSUAN_INVALID_FD; + unsigned int srvcount = 0; + int hostfound = 0; + int anyhostaddr = 0; + int srv, connected, v4_valid, v6_valid; + gpg_error_t last_err = 0; + struct srventry *serverlist = NULL; + + *r_sock = ASSUAN_INVALID_FD; + +#if defined(HAVE_W32_SYSTEM) && !defined(HTTP_NO_WSASTARTUP) + init_sockets (); +#endif /*Windows*/ + + check_inet_support (&v4_valid, &v6_valid); + + /* Onion addresses require special treatment. */ + if (is_onion_address (server)) + { +#ifdef ASSUAN_SOCK_TOR + + if (opt_debug) + log_debug ("http.c:connect_server:onion: name='%s' port=%hu\n", + server, port); + sock = assuan_sock_connect_byname (server, port, 0, NULL, + ASSUAN_SOCK_TOR); + if (sock == ASSUAN_INVALID_FD) + { + err = gpg_err_make (default_errsource, + (errno == EHOSTUNREACH)? GPG_ERR_UNKNOWN_HOST + : gpg_err_code_from_syserror ()); + log_error ("can't connect to '%s': %s\n", server, gpg_strerror (err)); + return err; + } + + notify_netactivity (); + *r_sock = sock; + return 0; + +#else /*!ASSUAN_SOCK_TOR*/ + + err = gpg_err_make (default_errsource, GPG_ERR_ENETUNREACH); + return ASSUAN_INVALID_FD; + +#endif /*!HASSUAN_SOCK_TOR*/ + } + + /* Do the SRV thing */ + if (srvtag) + { + err = get_dns_srv (server, srvtag, NULL, &serverlist, &srvcount); + if (err) + log_info ("getting '%s' SRV for '%s' failed: %s\n", + srvtag, server, gpg_strerror (err)); + /* Note that on error SRVCOUNT is zero. */ + err = 0; + } + + if (!serverlist) + { + /* Either we're not using SRV, or the SRV lookup failed. Make + up a fake SRV record. */ + serverlist = xtrycalloc (1, sizeof *serverlist); + if (!serverlist) + return gpg_err_make (default_errsource, gpg_err_code_from_syserror ()); + + serverlist->port = port; + strncpy (serverlist->target, server, DIMof (struct srventry, target)); + serverlist->target[DIMof (struct srventry, target)-1] = '\0'; + srvcount = 1; + } + + connected = 0; + for (srv=0; srv < srvcount && !connected; srv++) + { + dns_addrinfo_t aibuf, ai; + + if (opt_debug) + log_debug ("http.c:connect_server: trying name='%s' port=%hu\n", + serverlist[srv].target, port); + err = resolve_dns_name (serverlist[srv].target, port, 0, SOCK_STREAM, + &aibuf, NULL); + if (err) + { + log_info ("resolving '%s' failed: %s\n", + serverlist[srv].target, gpg_strerror (err)); + last_err = err; + continue; /* Not found - try next one. */ + } + hostfound = 1; + + for (ai = aibuf; ai && !connected; ai = ai->next) + { + if (ai->family == AF_INET + && ((flags & HTTP_FLAG_IGNORE_IPv4) || !v4_valid)) + continue; + if (ai->family == AF_INET6 + && ((flags & HTTP_FLAG_IGNORE_IPv6) || !v6_valid)) + continue; + + if (sock != ASSUAN_INVALID_FD) + assuan_sock_close (sock); + sock = my_sock_new_for_addr (ai->addr, ai->socktype, ai->protocol); + if (sock == ASSUAN_INVALID_FD) + { + if (errno == EAFNOSUPPORT) + { + if (ai->family == AF_INET) + v4_valid = 0; + if (ai->family == AF_INET6) + v6_valid = 0; + continue; + } + + err = gpg_err_make (default_errsource, + gpg_err_code_from_syserror ()); + log_error ("error creating socket: %s\n", gpg_strerror (err)); + free_dns_addrinfo (aibuf); + xfree (serverlist); + return err; + } + + anyhostaddr = 1; + err = connect_with_timeout (sock, (struct sockaddr *)ai->addr, + ai->addrlen, timeout); + if (err) + { + last_err = err; + } + else + { + connected = 1; + notify_netactivity (); + } + } + free_dns_addrinfo (aibuf); + } + + xfree (serverlist); + + if (!connected) + { + if (!hostfound) + log_error ("can't connect to '%s': %s\n", + server, "host not found"); + else if (!anyhostaddr) + log_error ("can't connect to '%s': %s\n", + server, "no IP address for host"); + else + { +#ifdef HAVE_W32_SYSTEM + log_error ("can't connect to '%s': ec=%d\n", + server, (int)WSAGetLastError()); +#else + log_error ("can't connect to '%s': %s\n", + server, gpg_strerror (last_err)); +#endif + } + err = last_err? last_err : gpg_err_make (default_errsource, + GPG_ERR_UNKNOWN_HOST); + if (sock != ASSUAN_INVALID_FD) + assuan_sock_close (sock); + return err; + } + + *r_sock = sock; + return 0; +} + + +/* Helper to read from a socket. This handles npth things and + * EINTR. */ +static gpgrt_ssize_t +read_server (assuan_fd_t sock, void *buffer, size_t size) +{ + int nread; + + do + { +#ifdef HAVE_W32_SYSTEM + /* Under Windows we need to use recv for a socket. */ +# if defined(USE_NPTH) + npth_unprotect (); +# endif + nread = recv (FD2INT (sock), buffer, size, 0); +# if defined(USE_NPTH) + npth_protect (); +# endif + +#else /*!HAVE_W32_SYSTEM*/ + +# ifdef USE_NPTH + nread = npth_read (sock, buffer, size); +# else + nread = read (sock, buffer, size); +# endif + +#endif /*!HAVE_W32_SYSTEM*/ + } + while (nread == -1 && errno == EINTR); + + return nread; +} + + +static gpg_error_t +write_server (assuan_fd_t sock, const char *data, size_t length) +{ + int nleft; + int nwritten; + + nleft = length; + while (nleft > 0) + { +#if defined(HAVE_W32_SYSTEM) +# if defined(USE_NPTH) + npth_unprotect (); +# endif + nwritten = send (FD2INT (sock), data, nleft, 0); +# if defined(USE_NPTH) + npth_protect (); +# endif + if ( nwritten == SOCKET_ERROR ) + { + log_info ("network write failed: ec=%d\n", (int)WSAGetLastError ()); + return gpg_error (GPG_ERR_NETWORK); + } +#else /*!HAVE_W32_SYSTEM*/ +# ifdef USE_NPTH + nwritten = npth_write (sock, data, nleft); +# else + nwritten = write (sock, data, nleft); +# endif + if (nwritten == -1) + { + if (errno == EINTR) + continue; + if (errno == EAGAIN) + { + struct timeval tv; + + tv.tv_sec = 0; + tv.tv_usec = 50000; + my_select (0, NULL, NULL, NULL, &tv); + continue; + } + log_info ("network write failed: %s\n", strerror (errno)); + return gpg_error_from_syserror (); + } +#endif /*!HAVE_W32_SYSTEM*/ + nleft -= nwritten; + data += nwritten; + } + + return 0; +} + + + +/* Read handler for estream. */ +static gpgrt_ssize_t +cookie_read (void *cookie, void *buffer, size_t size) +{ + cookie_t c = cookie; + int nread; + + if (c->content_length_valid) + { + if (!c->content_length) + return 0; /* EOF */ + if (c->content_length < size) + size = c->content_length; + } + +#if HTTP_USE_NTBTLS + if (c->use_tls && c->session && c->session->tls_session) + { + estream_t in, out; + + ntbtls_get_stream (c->session->tls_session, &in, &out); + nread = es_fread (buffer, 1, size, in); + if (opt_debug) + log_debug ("TLS network read: %d/%zu\n", nread, size); + } + else +#elif HTTP_USE_GNUTLS + if (c->use_tls && c->session && c->session->tls_session) + { + again: + nread = gnutls_record_recv (c->session->tls_session, buffer, size); + if (nread < 0) + { + if (nread == GNUTLS_E_INTERRUPTED) + goto again; + if (nread == GNUTLS_E_AGAIN) + { + struct timeval tv; + + tv.tv_sec = 0; + tv.tv_usec = 50000; + my_select (0, NULL, NULL, NULL, &tv); + goto again; + } + if (nread == GNUTLS_E_REHANDSHAKE) + goto again; /* A client is allowed to just ignore this request. */ + if (nread == GNUTLS_E_PREMATURE_TERMINATION) + { + /* The server terminated the connection. Close the TLS + session, and indicate EOF using a short read. */ + close_tls_session (c->session); + return 0; + } + log_info ("TLS network read failed: %s\n", gnutls_strerror (nread)); + gpg_err_set_errno (EIO); + return -1; + } + } + else +#endif /*HTTP_USE_GNUTLS*/ + { + nread = read_server (c->sock->fd, buffer, size); + } + + if (c->content_length_valid && nread > 0) + { + if (nread < c->content_length) + c->content_length -= nread; + else + c->content_length = 0; + } + + return (gpgrt_ssize_t)nread; +} + +/* Write handler for estream. */ +static gpgrt_ssize_t +cookie_write (void *cookie, const void *buffer_arg, size_t size) +{ + const char *buffer = buffer_arg; + cookie_t c = cookie; + int nwritten = 0; + +#if HTTP_USE_NTBTLS + if (c->use_tls && c->session && c->session->tls_session) + { + estream_t in, out; + + ntbtls_get_stream (c->session->tls_session, &in, &out); + if (size == 0) + es_fflush (out); + else + nwritten = es_fwrite (buffer, 1, size, out); + if (opt_debug) + log_debug ("TLS network write: %d/%zu\n", nwritten, size); + } + else +#elif HTTP_USE_GNUTLS + if (c->use_tls && c->session && c->session->tls_session) + { + int nleft = size; + while (nleft > 0) + { + nwritten = gnutls_record_send (c->session->tls_session, + buffer, nleft); + if (nwritten <= 0) + { + if (nwritten == GNUTLS_E_INTERRUPTED) + continue; + if (nwritten == GNUTLS_E_AGAIN) + { + struct timeval tv; + + tv.tv_sec = 0; + tv.tv_usec = 50000; + my_select (0, NULL, NULL, NULL, &tv); + continue; + } + log_info ("TLS network write failed: %s\n", + gnutls_strerror (nwritten)); + gpg_err_set_errno (EIO); + return -1; + } + nleft -= nwritten; + buffer += nwritten; + } + } + else +#endif /*HTTP_USE_GNUTLS*/ + { + if ( write_server (c->sock->fd, buffer, size) ) + { + gpg_err_set_errno (EIO); + nwritten = -1; + } + else + nwritten = size; + } + + return (gpgrt_ssize_t)nwritten; +} + + +#if defined(HAVE_W32_SYSTEM) && defined(HTTP_USE_NTBTLS) +static gpgrt_ssize_t +simple_cookie_read (void *cookie, void *buffer, size_t size) +{ + assuan_fd_t sock = (assuan_fd_t)cookie; + return read_server (sock, buffer, size); +} + +static gpgrt_ssize_t +simple_cookie_write (void *cookie, const void *buffer_arg, size_t size) +{ + assuan_fd_t sock = (assuan_fd_t)cookie; + const char *buffer = buffer_arg; + int nwritten; + + if (write_server (sock, buffer, size)) + { + gpg_err_set_errno (EIO); + nwritten = -1; + } + else + nwritten = size; + + return (gpgrt_ssize_t)nwritten; +} +#endif /*HAVE_W32_SYSTEM*/ + + +#ifdef HTTP_USE_GNUTLS +/* Wrapper for gnutls_bye used by my_socket_unref. */ +static void +send_gnutls_bye (void *opaque) +{ + tls_session_t tls_session = opaque; + int ret; + + again: + do + ret = gnutls_bye (tls_session, GNUTLS_SHUT_RDWR); + while (ret == GNUTLS_E_INTERRUPTED); + if (ret == GNUTLS_E_AGAIN) + { + struct timeval tv; + + tv.tv_sec = 0; + tv.tv_usec = 50000; + my_select (0, NULL, NULL, NULL, &tv); + goto again; + } +} +#endif /*HTTP_USE_GNUTLS*/ + +/* Close handler for estream. */ +static int +cookie_close (void *cookie) +{ + cookie_t c = cookie; + + if (!c) + return 0; + +#if HTTP_USE_NTBTLS + if (c->use_tls && c->session && c->session->tls_session) + { + /* FIXME!! Possibly call ntbtls_close_notify for close + of write stream. */ + my_socket_unref (c->sock, NULL, NULL); + } + else +#elif HTTP_USE_GNUTLS + if (c->use_tls && c->session && c->session->tls_session) + my_socket_unref (c->sock, send_gnutls_bye, c->session->tls_session); + else +#endif /*HTTP_USE_GNUTLS*/ + if (c->sock) + my_socket_unref (c->sock, NULL, NULL); + + if (c->session) + http_session_unref (c->session); + xfree (c); + return 0; +} + + + + +/* Verify the credentials of the server. Returns 0 on success and + store the result in the session object. */ +gpg_error_t +http_verify_server_credentials (http_session_t sess) +{ +#if HTTP_USE_GNUTLS + static const char errprefix[] = "TLS verification of peer failed"; + int rc; + unsigned int status; + const char *hostname; + const gnutls_datum_t *certlist; + unsigned int certlistlen; + gnutls_x509_crt_t cert; + gpg_error_t err = 0; + + sess->verify.done = 1; + sess->verify.status = 0; + sess->verify.rc = GNUTLS_E_CERTIFICATE_ERROR; + + if (gnutls_certificate_type_get (sess->tls_session) != GNUTLS_CRT_X509) + { + log_error ("%s: %s\n", errprefix, "not an X.509 certificate"); + sess->verify.rc = GNUTLS_E_UNSUPPORTED_CERTIFICATE_TYPE; + return gpg_error (GPG_ERR_GENERAL); + } + + rc = gnutls_certificate_verify_peers2 (sess->tls_session, &status); + if (rc) + { + log_error ("%s: %s\n", errprefix, gnutls_strerror (rc)); + if (!err) + err = gpg_error (GPG_ERR_GENERAL); + } + else if (status) + { + log_error ("%s: status=0x%04x\n", errprefix, status); +#if GNUTLS_VERSION_NUMBER >= 0x030104 + { + gnutls_datum_t statusdat; + + if (!gnutls_certificate_verification_status_print + (status, GNUTLS_CRT_X509, &statusdat, 0)) + { + log_info ("%s: %s\n", errprefix, statusdat.data); + gnutls_free (statusdat.data); + } + } +#endif /*gnutls >= 3.1.4*/ + + sess->verify.status = status; + if (!err) + err = gpg_error (GPG_ERR_GENERAL); + } + + hostname = sess->servername; + if (!hostname || !strchr (hostname, '.')) + { + log_error ("%s: %s\n", errprefix, "hostname missing"); + if (!err) + err = gpg_error (GPG_ERR_GENERAL); + } + + certlist = gnutls_certificate_get_peers (sess->tls_session, &certlistlen); + if (!certlistlen) + { + log_error ("%s: %s\n", errprefix, "server did not send a certificate"); + if (!err) + err = gpg_error (GPG_ERR_GENERAL); + + /* Need to stop here. */ + if (err) + return err; + } + + rc = gnutls_x509_crt_init (&cert); + if (rc < 0) + { + if (!err) + err = gpg_error (GPG_ERR_GENERAL); + if (err) + return err; + } + + rc = gnutls_x509_crt_import (cert, &certlist[0], GNUTLS_X509_FMT_DER); + if (rc < 0) + { + log_error ("%s: %s: %s\n", errprefix, "error importing certificate", + gnutls_strerror (rc)); + if (!err) + err = gpg_error (GPG_ERR_GENERAL); + } + + if (!gnutls_x509_crt_check_hostname (cert, hostname)) + { + log_error ("%s: %s\n", errprefix, "hostname does not match"); + if (!err) + err = gpg_error (GPG_ERR_GENERAL); + } + + gnutls_x509_crt_deinit (cert); + + if (!err) + sess->verify.rc = 0; + + if (sess->cert_log_cb) + { + const void *bufarr[10]; + size_t buflenarr[10]; + size_t n; + + for (n = 0; n < certlistlen && n < DIM (bufarr)-1; n++) + { + bufarr[n] = certlist[n].data; + buflenarr[n] = certlist[n].size; + } + bufarr[n] = NULL; + buflenarr[n] = 0; + sess->cert_log_cb (sess, err, hostname, bufarr, buflenarr); + } + + return err; +#else /*!HTTP_USE_GNUTLS*/ + (void)sess; + return gpg_error (GPG_ERR_NOT_IMPLEMENTED); +#endif +} + +/* Return the first query variable with the specified key. If there + is no such variable, return NULL. */ +struct uri_tuple_s * +uri_query_lookup (parsed_uri_t uri, const char *key) +{ + struct uri_tuple_s *t; + + for (t = uri->query; t; t = t->next) + if (strcmp (t->name, key) == 0) + return t; + + return NULL; +} + +const char * +uri_query_value (parsed_uri_t url, const char *key) +{ + struct uri_tuple_s *t; + t = uri_query_lookup (url, key); + return t? t->value : NULL; +} + + + +/* Return true if both URI point to the same host for the purpose of + * redirection check. A is the original host and B the host given in + * the Location header. As a temporary workaround a fixed list of + * exceptions is also consulted. */ +static int +same_host_p (parsed_uri_t a, parsed_uri_t b) +{ + static struct + { + const char *from; /* NULL uses the last entry from the table. */ + const char *to; + } allow[] = + { + { "protonmail.com", "api.protonmail.com" }, + { NULL, "api.protonmail.ch" }, + { "protonmail.ch", "api.protonmail.com" }, + { NULL, "api.protonmail.ch" }, + { "pm.me", "api.protonmail.ch" } + }; + static const char *subdomains[] = + { + "openpgpkey." + }; + int i; + const char *from; + + if (!a->host || !b->host) + return 0; + + if (!ascii_strcasecmp (a->host, b->host)) + return 1; + + from = NULL; + for (i=0; i < DIM (allow); i++) + { + if (allow[i].from) + from = allow[i].from; + if (!from) + continue; + if (!ascii_strcasecmp (from, a->host) + && !ascii_strcasecmp (allow[i].to, b->host)) + return 1; + } + + /* Also consider hosts the same if they differ only in a subdomain; + * in both direction. This allows to have redirection between the + * WKD advanced and direct lookup methods. */ + for (i=0; i < DIM (subdomains); i++) + { + const char *subdom = subdomains[i]; + size_t subdomlen = strlen (subdom); + + if (!ascii_strncasecmp (a->host, subdom, subdomlen) + && !ascii_strcasecmp (a->host + subdomlen, b->host)) + return 1; + if (!ascii_strncasecmp (b->host, subdom, subdomlen) + && !ascii_strcasecmp (b->host + subdomlen, a->host)) + return 1; + } + + return 0; +} + + +/* Prepare a new URL for a HTTP redirect. INFO has flags controlling + * the operation, STATUS_CODE is used for diagnostics, LOCATION is the + * value of the "Location" header, and R_URL reveives the new URL on + * success or NULL or error. Note that INFO->ORIG_URL is + * required. */ +gpg_error_t +http_prepare_redirect (http_redir_info_t *info, unsigned int status_code, + const char *location, char **r_url) +{ + gpg_error_t err; + parsed_uri_t locuri; + parsed_uri_t origuri; + char *newurl; + char *p; + + *r_url = NULL; + + if (!info || !info->orig_url) + return gpg_error (GPG_ERR_INV_ARG); + + if (!info->silent) + log_info (_("URL '%s' redirected to '%s' (%u)\n"), + info->orig_url, location? location:"[none]", status_code); + + if (!info->redirects_left) + { + if (!info->silent) + log_error (_("too many redirections\n")); + return gpg_error (GPG_ERR_NO_DATA); + } + info->redirects_left--; + + if (!location || !*location) + return gpg_error (GPG_ERR_NO_DATA); + + err = http_parse_uri (&locuri, location, 0); + if (err) + return err; + + /* Make sure that an onion address only redirects to another + * onion address, or that a https address only redirects to a + * https address. */ + if (info->orig_onion && !locuri->onion) + { + dirmngr_status_printf (info->ctrl, "WARNING", + "http_redirect %u" + " redirect from onion to non-onion address" + " rejected", + err); + http_release_parsed_uri (locuri); + return gpg_error (GPG_ERR_FORBIDDEN); + } + if (!info->allow_downgrade && info->orig_https && !locuri->use_tls) + { + err = gpg_error (GPG_ERR_FORBIDDEN); + dirmngr_status_printf (info->ctrl, "WARNING", + "http_redirect %u" + " redirect '%s' to '%s' rejected", + err, info->orig_url, location); + http_release_parsed_uri (locuri); + return err; + } + + if (info->trust_location) + { + /* We trust the Location - return it verbatim. */ + http_release_parsed_uri (locuri); + newurl = xtrystrdup (location); + if (!newurl) + { + err = gpg_error_from_syserror (); + http_release_parsed_uri (locuri); + return err; + } + } + else if ((err = http_parse_uri (&origuri, info->orig_url, 0))) + { + http_release_parsed_uri (locuri); + return err; + } + else if (same_host_p (origuri, locuri)) + { + /* The host is the same or on an exception list and thus we can + * take the location verbatim. */ + http_release_parsed_uri (origuri); + http_release_parsed_uri (locuri); + newurl = xtrystrdup (location); + if (!newurl) + { + err = gpg_error_from_syserror (); + http_release_parsed_uri (locuri); + return err; + } + } + else + { + /* We take only the host and port from the URL given in the + * Location. This limits the effects of redirection attacks by + * rogue hosts returning an URL to servers in the client's own + * network. We don't even include the userinfo because they + * should be considered similar to the path and query parts. + */ + if (!(locuri->off_path - locuri->off_host)) + { + http_release_parsed_uri (origuri); + http_release_parsed_uri (locuri); + return gpg_error (GPG_ERR_BAD_URI); + } + if (!(origuri->off_path - origuri->off_host)) + { + http_release_parsed_uri (origuri); + http_release_parsed_uri (locuri); + return gpg_error (GPG_ERR_BAD_URI); + } + + newurl = xtrymalloc (strlen (origuri->original) + + (locuri->off_path - locuri->off_host) + 1); + if (!newurl) + { + err = gpg_error_from_syserror (); + http_release_parsed_uri (origuri); + http_release_parsed_uri (locuri); + return err; + } + /* Build new URL from + * uriguri: scheme userinfo ---- ---- path rest + * locuri: ------ -------- host port ---- ---- + */ + p = newurl; + memcpy (p, origuri->original, origuri->off_host); + p += origuri->off_host; + memcpy (p, locuri->original + locuri->off_host, + (locuri->off_path - locuri->off_host)); + p += locuri->off_path - locuri->off_host; + strcpy (p, origuri->original + origuri->off_path); + + http_release_parsed_uri (origuri); + http_release_parsed_uri (locuri); + if (!info->silent) + log_info (_("redirection changed to '%s'\n"), newurl); + dirmngr_status_printf (info->ctrl, "WARNING", + "http_redirect_cleanup %u" + " changed from '%s' to '%s'", + 0, info->orig_url, newurl); + } + + *r_url = newurl; + return 0; +} + + +/* Return string describing the http STATUS. Returns an empty string + * for an unknown status. */ +const char * +http_status2string (unsigned int status) +{ + switch (status) + { + case 500: return "Internal Server Error"; + case 501: return "Not Implemented"; + case 502: return "Bad Gateway"; + case 503: return "Service Unavailable"; + case 504: return "Gateway Timeout"; + case 505: return "HTTP version Not Supported"; + case 506: return "Variant Also Negation"; + case 507: return "Insufficient Storage"; + case 508: return "Loop Detected"; + case 510: return "Not Extended"; + case 511: return "Network Authentication Required"; + } + + return ""; +} -- cgit v1.2.3