Newer
Older
XinYang_IOS / Pods / OpenVPNAdapter / Sources / OpenVPN3 / openvpn / ws / httpcli.hpp
@zhangfeng zhangfeng on 7 Dec 2023 33 KB 1.8.0
//    OpenVPN -- An application to securely tunnel IP networks
//               over a single port, with support for SSL/TLS-based
//               session authentication and key exchange,
//               packet encryption, packet authentication, and
//               packet compression.
//
//    Copyright (C) 2012-2020 OpenVPN Inc.
//
//    This program is free software: you can redistribute it and/or modify
//    it under the terms of the GNU Affero General Public License Version 3
//    as published by the Free Software Foundation.
//
//    This program 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 Affero General Public License for more details.
//
//    You should have received a copy of the GNU Affero General Public License
//    along with this program in the COPYING file.
//    If not, see <http://www.gnu.org/licenses/>.

#pragma once

// General purpose HTTP/HTTPS/Web-services client.
// Supports:
//   * asynchronous I/O through Asio
//   * http/https
//   * chunking
//   * keepalive
//   * connect and overall timeouts
//   * GET, POST, etc.
//   * any OpenVPN SSL module (OpenSSL, MbedTLS)
//   * server CA bundle
//   * client certificate
//   * HTTP basic auth
//   * limits on content-size, header-size, and number of headers
//   * cURL not needed
//
//  See test/ws/wstest.cpp for usage examples including Dropwizard REST/JSON API client.
//  See test/ws/asprof.cpp for sample AS REST API client.

#include <string>
#include <vector>
#include <sstream>
#include <ostream>
#include <cstdint>
#include <utility>
#include <memory>
#include <algorithm>         // for std::min, std::max

#ifdef USE_ASYNC_RESOLVE
#include <openvpn/client/async_resolve.hpp>
#endif

#include <openvpn/common/platform.hpp>
#include <openvpn/common/base64.hpp>
#include <openvpn/common/olong.hpp>
#include <openvpn/common/arraysize.hpp>
#include <openvpn/common/hostport.hpp>
#include <openvpn/common/base64.hpp>
#include <openvpn/addr/ip.hpp>
#include <openvpn/asio/asiopolysock.hpp>
#include <openvpn/asio/asioresolverres.hpp>
#include <openvpn/common/to_string.hpp>
#include <openvpn/error/error.hpp>
#include <openvpn/buffer/bufstream.hpp>
#include <openvpn/http/reply.hpp>
#include <openvpn/time/asiotimersafe.hpp>
#include <openvpn/time/coarsetime.hpp>
#include <openvpn/transport/tcplink.hpp>
#include <openvpn/transport/client/transbase.hpp>
#include <openvpn/ws/httpcommon.hpp>
#include <openvpn/ws/httpcreds.hpp>
#include <openvpn/ws/websocket.hpp>

#if defined(OPENVPN_POLYSOCK_SUPPORTS_ALT_ROUTING)
#include <openvpn/asio/alt_routing.hpp>
#endif

#if defined(OPENVPN_PLATFORM_WIN)
#include <openvpn/win/scoped_handle.hpp>
#include <openvpn/win/winerr.hpp>
#endif

#ifdef SIMULATE_HTTPCLI_FAILURES // debugging -- simulate network failures
#include <openvpn/common/periodic_fail.hpp>
#endif

namespace openvpn {
  namespace WS {
    namespace Client {

      OPENVPN_EXCEPTION(http_client_exception);

      struct Status
      {
	// Error codes
	enum {
	  E_SUCCESS=0,
	  E_RESOLVE,
	  E_CONNECT,
	  E_TRANSPORT,
	  E_PROXY,
	  E_TCP,
	  E_HTTP,
	  E_EXCEPTION,
	  E_BAD_REQUEST,
	  E_HEADER_SIZE,
	  E_CONTENT_SIZE,
	  E_CONTENT_TYPE,
	  E_EOF_SSL,
	  E_EOF_TCP,
	  E_CONNECT_TIMEOUT,
	  E_GENERAL_TIMEOUT,
	  E_KEEPALIVE_TIMEOUT,
	  E_SHUTDOWN,
	  E_ABORTED,
	  E_BOGON, // simulated fault injection for testing

	  N_ERRORS
	};

	static std::string error_str(const int status)
	{
	  static const char *error_names[] = {
	    "E_SUCCESS",
	    "E_RESOLVE",
	    "E_CONNECT",
	    "E_TRANSPORT",
	    "E_PROXY",
	    "E_TCP",
	    "E_HTTP",
	    "E_EXCEPTION",
	    "E_BAD_REQUEST",
	    "E_HEADER_SIZE",
	    "E_CONTENT_SIZE",
	    "E_CONTENT_TYPE",
	    "E_EOF_SSL",
	    "E_EOF_TCP",
	    "E_CONNECT_TIMEOUT",
	    "E_GENERAL_TIMEOUT",
	    "E_KEEPALIVE_TIMEOUT",
	    "E_SHUTDOWN",
	    "E_ABORTED",
	    "E_BOGON",
	  };

	  static_assert(N_ERRORS == array_size(error_names), "HTTP error names array inconsistency");
	  if (status >= 0 && status < N_ERRORS)
	    return error_names[status];
	  else if (status == -1)
	    return "E_UNDEF";
	  else
	    return "E_?/" + openvpn::to_string(status);
	}

	static bool is_error(const int status)
	{
	  switch (status)
	    {
	    case E_SUCCESS:
	    case E_SHUTDOWN:
	      return false;
	    default:
	      return true;
	    }
	}
      };

      struct Host;

#ifdef OPENVPN_POLYSOCK_SUPPORTS_ALT_ROUTING
      struct AltRoutingShimFactory : public RC<thread_unsafe_refcount>
      {
	typedef RCPtr<AltRoutingShimFactory> Ptr;

	virtual AltRouting::Shim::Ptr shim(const Host& host) = 0;
	virtual void report_error(const Host& host, const bool alt_routing) {}
	virtual bool is_reset(const Host& host, const bool alt_routing) { return false; }
	virtual int connect_timeout() { return -1; }
	virtual IP::Addr remote_ip() { return IP::Addr(); }
	virtual int remote_port() { return -1; }
	virtual int error_expire() { return 0; }
      };
#endif

      struct Config : public RCCopyable<thread_unsafe_refcount>
      {
	typedef RCPtr<Config> Ptr;

	SSLFactoryAPI::Ptr ssl_factory;
	TransportClientFactory::Ptr transcli;
	std::string user_agent;
	unsigned int connect_timeout = 0;
	unsigned int general_timeout = 0;
	unsigned int keepalive_timeout = 0;
	unsigned int max_headers = 0;
	unsigned int max_header_bytes = 0;
	bool enable_cache = false; // if true, supports TLS session resumption tickets
	olong max_content_bytes = 0;
	unsigned int msg_overhead_bytes = 0;
	int debug_level = 0;
	Frame::Ptr frame;
	SessionStats::Ptr stats;

#ifdef OPENVPN_POLYSOCK_SUPPORTS_ALT_ROUTING
	AltRoutingShimFactory::Ptr shim_factory;
#endif
      };

      struct Host {
	std::string host;
	std::string hint;   // overrides host for transport, may be IP address
	std::string cn;     // host for CN verification, defaults to host if empty
	std::string key;    // host for TLS session ticket cache key, defaults to host if empty
	std::string head;   // host to send in HTTP header, defaults to host if empty
	std::string port;

	std::string local_addr;  // bind to local IP addr (optional)
	std::string local_port;  // bind to local port (optional)

	const std::string& host_transport() const
	{
	  return hint.empty() ? host : hint;
	}

	const std::string* host_cn_ptr() const
	{
	  return cn.empty() ? &host : &cn;
	}

	const std::string& host_cn() const
	{
	  return *host_cn_ptr();
	}

	const std::string& host_head() const
	{
	  return head.empty() ? host : head;
	}

	std::string host_port_str() const
	{
	  std::string ret;
	  const std::string& ht = host_transport();
	  if (ht == host)
	    {
	      ret += '[';
	      ret += host;
	      ret += "]:";
	      ret += port;
	    }
	  else
	    {
	      ret += host;
	      ret += '[';
	      ret += ht;
	      ret += "]:";
	      ret += port;
	    }
	  return ret;
	}

	std::string cache_key() const
	{
	  return key.empty() ? (host + '/' + port ) : key;
	}
      };

      struct Request
      {
	bool creds_defined() const
	{
	  return !username.empty() || !password.empty();
	}

	void set_creds(const Creds& creds)
	{
	  username = creds.username;
	  password = creds.password;
	}

	std::string method;
	std::string uri;
	std::string username;
	std::string password;
      };

      struct ContentInfo {
	// content length if Transfer-Encoding: chunked
	static constexpr olong CHUNKED = -1;

	std::string type;
	std::string content_encoding;
	olong length = 0;
	bool keepalive = false;
	bool lean_headers = false;
	std::vector<std::string> extra_headers;
	WebSocket::Client::PerRequest::Ptr websocket;
      };

      struct TimeoutOverride
      {
	// Timeout overrides in seconds.
	// Set to -1 to disable.
	int connect = -1;
	int general = -1;
	int keepalive = -1;
      };

      class HTTPCore;
      typedef HTTPBase<HTTPCore, Config, Status, HTTP::ReplyType, ContentInfo, olong, RC<thread_unsafe_refcount>> Base;

      class HTTPCore : public Base, public TransportClientParent
#ifdef USE_ASYNC_RESOLVE
		     , public AsyncResolvableTCP
#endif
      {
      public:
	friend Base;

	typedef RCPtr<HTTPCore> Ptr;

	struct AsioProtocol
	{
	  typedef AsioPolySock::Base socket;
	};

	HTTPCore(openvpn_io::io_context& io_context_arg,
		 Config::Ptr config_arg)
	  : Base(std::move(config_arg)),
#ifdef USE_ASYNC_RESOLVE
	    AsyncResolvableTCP(io_context_arg),
#endif
	    io_context(io_context_arg),
#ifndef USE_ASYNC_RESOLVE
	    resolver(io_context_arg),
#endif
	    connect_timer(io_context_arg),
	    general_timer(io_context_arg),
	    general_timeout_coarse(Time::Duration::binary_ms(512), Time::Duration::binary_ms(1024))
	{
	}

	virtual ~HTTPCore()
	{
	  stop(false);
	}

	// Should be called before start_request().
	void override_timeouts(TimeoutOverride to_arg)
	{
	  to = std::move(to_arg);
	}

	bool is_alive() const
	{
	  return alive;
	}

	bool is_link_active()
	{
	  return link && !halt;
	}

	// return true if the alt-routing state for this session
	// has changed, requiring a reset
	bool is_alt_routing_reset() const
	{
#ifdef OPENVPN_POLYSOCK_SUPPORTS_ALT_ROUTING
	  if (config->shim_factory
	   && socket
	   && config->shim_factory->is_reset(host, socket->alt_routing_enabled()))
	    return true;
#endif
	  return false;
	}

	void check_ready() const
	{
	  if (!is_ready())
	    throw http_client_exception("not ready");
	}

	void start_request()
	{
	  check_ready();
	  ready = false;
	  cancel_keepalive_timer();
	  openvpn_io::post(io_context, [self=Ptr(this)]()
		     {
		       self->handle_request();
		     });
	}

	void start_request_after(const Time::Duration dur)
	{
	  check_ready();
	  ready = false;
	  cancel_keepalive_timer();
	  if (!req_timer)
	    req_timer.reset(new AsioTimerSafe(io_context));
	  req_timer->expires_after(dur);
	  req_timer->async_wait([self=Ptr(this)](const openvpn_io::error_code& error)
				{
				  if (!error)
				    self->handle_request();
				});
	}

	void stop(const bool shutdown)
	{
	  if (!halt)
	    {
	      halt = true;
	      ready = false;
	      alive = false;
	      if (transcli)
		transcli->stop();
	      if (link)
		link->stop();
	      if (socket)
		{
		  if (shutdown)
		    socket->shutdown(AsioPolySock::SHUTDOWN_SEND|AsioPolySock::SHUTDOWN_RECV);
		  socket->close();
		}
#ifdef USE_ASYNC_RESOLVE
	      async_resolve_cancel();
#else
	      resolver.cancel();
#endif
	      if (req_timer)
		req_timer->cancel();
	      cancel_keepalive_timer();
	      general_timer.cancel();
	      connect_timer.cancel();
	    }
	}

	void abort(const std::string& message, const int status=Status::E_ABORTED)
	{
	  if (!halt)
	    error_handler(status, message);
	}

	const HTTP::Reply& reply() const {
	  return request_reply();
	}

	std::string remote_endpoint_str() const
	{
	  try {
	    if (socket)
	      return socket->remote_endpoint_str();
	  }
	  catch (const std::exception& e)
	    {
	    }
	  return "[unknown endpoint]";
	}

	bool remote_ip_port(IP::Addr& addr, unsigned int& port) const
	{
	  if (socket)
	    return socket->remote_ip_port(addr, port);
	  else
	    return false;
	}

	// Return the current Host object, but
	// set the hint/port fields to the live
	// IP address/port of the connection.
	Host host_hint()
	{
	  Host h = host;
	  if (socket)
	    {
	      IP::Addr addr;
	      unsigned int port;
	      if (socket->remote_ip_port(addr, port))
		{
		  h.hint = addr.to_string();
		  h.port = openvpn::to_string(port);
		}
	    }
	  return h;
	}

	bool host_match(const std::string& host_arg) const
	{
	  if (host.host.empty())
	    return false;
	  else
	    return host.host == host_arg;
	}

	AsioPolySock::Base* get_socket()
	{
	  return socket.get();
	}

	void streaming_start()
	{
	  cancel_general_timeout();
	  content_out_hold = false;
	  if (is_deferred())
	    http_content_out_needed();
	}

	void streaming_restart()
	{
	  if (content_out_hold)
	    throw http_client_exception("streaming_restart() called when content-out is still in hold state");
	  if (is_deferred())
	    http_content_out_needed();
	}

	bool is_streaming_restartable() const
	{
	  return !content_out_hold;
	}

	bool is_streaming_hold() const
	{
	  return content_out_hold;
	}

	// virtual methods

	virtual Host http_host() = 0;

	virtual Request http_request() = 0;

	virtual ContentInfo http_content_info()
	{
	  return ContentInfo();
	}

	virtual BufferPtr http_content_out()
	{
	  return BufferPtr();
	}

	virtual void http_content_out_needed()
	{
	}

	virtual void http_headers_received()
	{
	}

	virtual void http_headers_sent(const Buffer& buf)
	{
	}

	virtual void http_mutate_resolver_results(openvpn_io::ip::tcp::resolver::results_type& results)
	{
	}

	virtual void http_content_in(BufferAllocated& buf) = 0;

	virtual void http_done(const int status, const std::string& description) = 0;

	virtual void http_keepalive_close(const int status, const std::string& description)
	{
	}

	virtual void http_post_connect(AsioPolySock::Base& sock)
	{
	}

      private:
	typedef TCPTransport::Link<AsioProtocol, HTTPCore*, false> LinkImpl;
	friend LinkImpl::Base; // calls tcp_* handlers

	void verify_frame()
	{
	  if (!frame)
	    throw http_client_exception("frame undefined");
	}

#ifdef SIMULATE_HTTPCLI_FAILURES // debugging -- simulate network failures
	bool inject_fault(const char *caller)
	{
	  if (periodic_fail.trigger("httpcli", SIMULATE_HTTPCLI_FAILURES))
	    {
	      OPENVPN_LOG("HTTPCLI BOGON on " << host.host_port_str() << " (" << caller << ')');
	      error_handler(Status::E_BOGON, caller);
	      return true;
	    }
	  else
	    return false;
	}
#endif

	void activity(const bool init)
	{
	  const Time now = Time::now();
	  if (general_timeout_duration.defined())
	    {
	      const Time next = now + general_timeout_duration;
	      if (init || !general_timeout_coarse.similar(next))
		{
		  general_timeout_coarse.reset(next);
		  general_timer.expires_at(next);
		  general_timer.async_wait([self=Ptr(this)](const openvpn_io::error_code& error)
					   {
					     if (!error)
					       self->general_timeout_handler(error);
					   });
		}
	    }
	  else if (init)
	    {
	      general_timeout_coarse.reset();
	      general_timer.cancel();
	    }
	}

	void handle_request() // called by Asio
	{
	  if (halt)
	    return;

	  try {
	    if (ready)
	      throw http_client_exception("handle_request called in ready state");

	    verify_frame();

	    general_timeout_duration = Time::Duration::seconds(to.general >= 0
							       ? to.general
							       : config->general_timeout);
	    general_timeout_coarse.reset();
	    activity(true);

	    // already in persistent session?
	    if (alive)
	      {
		generate_request();
		return;
	      }

	    host = http_host();

#ifdef ASIO_HAS_LOCAL_SOCKETS
	    // unix domain socket?
	    if (host.port == "unix")
	      {
		openvpn_io::local::stream_protocol::endpoint ep(host.host_transport());
		AsioPolySock::Unix* s = new AsioPolySock::Unix(io_context, 0);
		socket.reset(s);
		s->socket.async_connect(ep,
					[self=Ptr(this)](const openvpn_io::error_code& error)
					{
					  self->handle_unix_connect(error);
					});
		set_connect_timeout(config->connect_timeout);
		return;
	      }
#endif
#ifdef OPENVPN_PLATFORM_WIN
	    // windows named pipe?
	    if (host.port == "np")
	      {
		const std::string& ht = host.host_transport();
		const HANDLE h = ::CreateFileA(
					       ht.c_str(),
					       GENERIC_READ | GENERIC_WRITE,
					       0,
					       NULL,
					       OPEN_EXISTING,
					       FILE_FLAG_OVERLAPPED,
					       NULL);
		if (!Win::Handle::defined(h))
		  {
		    const Win::LastError err;
		    OPENVPN_THROW(http_client_exception, "failed to open existing named pipe: " << ht << " : " << err.message());
		  }
		socket.reset(new AsioPolySock::NamedPipe(openvpn_io::windows::stream_handle(io_context, h), 0));
		do_connect(true);
		set_connect_timeout(config->connect_timeout);
		return;
	      }
#endif
#if defined(OPENVPN_POLYSOCK_SUPPORTS_ALT_ROUTING)
	    // alt routing?
	    if (config->shim_factory)
	      {
		AltRouting::Shim::Ptr shim = config->shim_factory->shim(host);
		if (shim)
		  {
		    alt_routing_connect(std::move(shim));
		    return;
		  }
	      }
#endif

	    // standard TCP (with or without SSL)
	    if (host.port.empty())
	      host.port = config->ssl_factory ? "443" : "80";

	    if (config->ssl_factory)
	      {
		if (config->enable_cache)
		  {
		    std::string cache_key = host.cache_key();
		    ssl_sess = config->ssl_factory->ssl(host.host_cn_ptr(), &cache_key);
		  }
		else
		  ssl_sess = config->ssl_factory->ssl(host.host_cn_ptr(), nullptr);
	      }

	    if (config->transcli)
	      {
		transcli = config->transcli->new_transport_client_obj(io_context, this);
		transcli->transport_start();
	      }
	    else
	      {
#ifdef USE_ASYNC_RESOLVE
		async_resolve_name(host.host_transport(), host.port);
#else
		resolver.async_resolve(host.host_transport(), host.port,
				       [self=Ptr(this)](const openvpn_io::error_code& error, openvpn_io::ip::tcp::resolver::results_type results)
				       {
					 self->resolve_callback(error, results);
				       });
#endif
	      }
	    set_connect_timeout(config->connect_timeout);
	  }
	  catch (const std::exception& e)
	    {
	      handle_exception("handle_request", e);
	    }
	}

	void resolve_callback(const openvpn_io::error_code& error, // called by Asio
			      openvpn_io::ip::tcp::resolver::results_type results)
	{
	  if (halt)
	    return;

#ifdef SIMULATE_HTTPCLI_FAILURES // debugging -- simulate network failures
	  if (inject_fault("resolve_callback"))
	      return;
#endif

	  if (error)
	    {
	      asio_error_handler(Status::E_RESOLVE, "resolve_callback", error);
	      return;
	    }

	  try {
	    http_mutate_resolver_results(results);
	    if (results.empty())
	      OPENVPN_THROW_EXCEPTION("no results");

	    AsioPolySock::TCP* s = new AsioPolySock::TCP(io_context, 0);
	    socket.reset(s);
	    bind_local_addr(s);

	    if (config->debug_level >= 2)
	      OPENVPN_LOG("TCP HTTP CONNECT to " << s->remote_endpoint_str() << " res=" << asio_resolver_results_to_string(results));

	    openvpn_io::async_connect(s->socket, std::move(results),
				[self=Ptr(this)](const openvpn_io::error_code& error, const openvpn_io::ip::tcp::endpoint& endpoint)
				{
				  self->handle_tcp_connect(error, endpoint);
				});
	  }
	  catch (const std::exception& e)
	    {
	      handle_exception("resolve_callback", e);
	    }
	}

	void handle_tcp_connect(const openvpn_io::error_code& error, // called by Asio
				const openvpn_io::ip::tcp::endpoint& endpoint)
	{
	  if (halt)
	    return;

#ifdef SIMULATE_HTTPCLI_FAILURES // debugging -- simulate network failures
	  if (inject_fault("handle_tcp_connect"))
	      return;
#endif

	  if (error)
	    {
	      asio_error_handler(Status::E_CONNECT, "handle_tcp_connect", error);
	      return;
	    }

	  try {
	    do_connect(true);
	  }
	  catch (const std::exception& e)
	    {
	      handle_exception("handle_tcp_connect", e);
	    }
	}

#ifdef ASIO_HAS_LOCAL_SOCKETS
	void handle_unix_connect(const openvpn_io::error_code& error) // called by Asio
	{
	  if (halt)
	    return;

#ifdef SIMULATE_HTTPCLI_FAILURES // debugging -- simulate network failures
	  if (inject_fault("handle_unix_connect"))
	      return;
#endif

	  if (error)
	    {
	      asio_error_handler(Status::E_CONNECT, "handle_unix_connect", error);
	      return;
	    }

	  try {
	    do_connect(true);
	  }
	  catch (const std::exception& e)
	    {
	      handle_exception("handle_unix_connect", e);
	    }
	}
#endif

#if defined(OPENVPN_POLYSOCK_SUPPORTS_ALT_ROUTING)
	void alt_routing_connect(AltRouting::Shim::Ptr shim)
	{
	  AltRoutingShimFactory& sf = *config->shim_factory;

	  // build socket and assign shim
	  AsioPolySock::TCP* s = new AsioPolySock::TCP(io_context, 0);
	  socket.reset(s);
	  bind_local_addr(s);
	  s->socket.shim = std::move(shim);

	  // build results
	  int port = sf.remote_port();
	  if (port < 0)
	    port = HostPort::parse_port(host.port, "AltRouting");
	  IP::Addr addr = sf.remote_ip();
	  if (!addr.defined())
	    addr = IP::Addr(host.host_transport(), "AltRouting");
	  openvpn_io::ip::tcp::resolver::results_type results =
	    openvpn_io::ip::tcp::resolver::results_type::create(openvpn_io::ip::tcp::endpoint(addr.to_asio(),
											      port),
								host.host,
								"");

	  if (config->debug_level >= 2)
	    OPENVPN_LOG("ALT_ROUTING HTTP CONNECT to " << s->remote_endpoint_str() << " res=" << asio_resolver_results_to_string(results));

	  // do async connect
	  openvpn_io::async_connect(s->socket, std::move(results),
				    [self=Ptr(this)](const openvpn_io::error_code& error, const openvpn_io::ip::tcp::endpoint& endpoint)
				    {
				      self->handle_tcp_connect(error, endpoint);
				    });

	  // set connect timeout
	  {
	    unsigned int ct = sf.connect_timeout();
	    if (ct < 0)
	      ct = config->connect_timeout;
	    set_connect_timeout(ct);
	  }
	}
#endif

	void do_connect(const bool use_link)
	{
	  connect_timer.cancel();
	  set_default_stats();

	  if (use_link)
	    {
	      socket->set_cloexec();
	      socket->tcp_nodelay();
	      http_post_connect(*socket);
	      link.reset(new LinkImpl(this,
				      *socket,
				      0, // send_queue_max_size (unlimited)
				      8, // free_list_max_size
				      (*frame)[Frame::READ_HTTP],
				      stats));
	      link->set_raw_mode(true);
	      link->start();
	    }

	  if (ssl_sess)
	    ssl_sess->start_handshake();

	  // xmit the request
	  generate_request();
	}

	void set_connect_timeout(unsigned int connect_timeout)
	{
	  if (config->connect_timeout)
	    {
	      connect_timer.expires_after(Time::Duration::seconds(to.connect >= 0
								  ? to.connect
								  : connect_timeout));
	      connect_timer.async_wait([self=Ptr(this)](const openvpn_io::error_code& error)
				       {
					 if (!error)
					   self->connect_timeout_handler(error);
				       });
	    }
	}

	void bind_local_addr(AsioPolySock::TCP* s)
	{
	  // optionally bind to local addr/port
	  if (!host.local_addr.empty())
	    {
#if defined(OPENVPN_POLYSOCK_SUPPORTS_BIND) || defined(OPENVPN_POLYSOCK_SUPPORTS_ALT_ROUTING)
	      const IP::Addr local_addr(host.local_addr, "local_addr");
	      unsigned short local_port = 0;
	      if (!host.local_port.empty())
		local_port = HostPort::parse_port(host.local_port, "local_port");
	      s->socket.bind_local(local_addr, local_port);
#else
	      throw Exception("httpcli must be built with OPENVPN_POLYSOCK_SUPPORTS_BIND or OPENVPN_POLYSOCK_SUPPORTS_ALT_ROUTING to support local bind");
#endif
	    }
	}

	void schedule_keepalive_timer()
	{
	  if (config->keepalive_timeout || to.keepalive >= 0)
	    {
	      const Time::Duration dur = Time::Duration::seconds(to.keepalive >= 0
								 ? to.keepalive
								 : config->keepalive_timeout);
	      if (!keepalive_timer)
		keepalive_timer.reset(new AsioTimerSafe(io_context));
	      keepalive_timer->expires_after(dur);
	      keepalive_timer->async_wait([self=Ptr(this)](const openvpn_io::error_code& error)
	              {
			if (!self->halt && !error && self->ready)
			  {
			    self->error_handler(Status::E_KEEPALIVE_TIMEOUT, "Keepalive timeout");
			  }
		      });
	    }
	}

	void cancel_keepalive_timer()
	{
	  if (keepalive_timer)
	    keepalive_timer->cancel();
	}

	void cancel_general_timeout()
	{
	  general_timeout_duration.set_zero();
	  general_timer.cancel();
	}

	void general_timeout_handler(const openvpn_io::error_code& e) // called by Asio
	{
	  if (!halt && !e)
	    error_handler(Status::E_GENERAL_TIMEOUT, "General timeout");
	}

	void connect_timeout_handler(const openvpn_io::error_code& e) // called by Asio
	{
	  if (!halt && !e)
	    error_handler(Status::E_CONNECT_TIMEOUT, "Connect timeout");
	}

	void set_default_stats()
	{
	  if (!stats)
	    stats.reset(new SessionStats());
	}

	void generate_request()
	{
	  rr_reset();
	  http_out_begin();

	  const Request req = http_request();
	  content_info = http_content_info();

	  outbuf.reset(new BufferAllocated(512, BufferAllocated::GROW));
	  BufferStreamOut os(*outbuf);

	  if (content_info.websocket)
	    {
	      // no content-out until after server reply (content_out_hold kept at true)
	      generate_request_websocket(os, req);
	    }
	  else
	    {
	      // non-websocket allows immediate content-out
	      content_out_hold = false;
	      generate_request_http(os, req);
	    }

	  http_headers_sent(*outbuf);
	  http_out();
	}

	void generate_request_http(std::ostream& os, const Request& req)
	{
	  os << req.method << ' ' << req.uri << " HTTP/1.1\r\n";
	  if (!content_info.lean_headers)
	    {
	      os << "Host: " << host.host_head() << "\r\n";
	      if (!config->user_agent.empty())
		os << "User-Agent: " << config->user_agent << "\r\n";
	    }
	  generate_basic_auth_headers(os, req);
	  if (content_info.length)
	    os << "Content-Type: " << content_info.type << "\r\n";
	  if (content_info.length > 0)
	    os << "Content-Length: " << content_info.length << "\r\n";
	  else if (content_info.length == ContentInfo::CHUNKED)
	    os << "Transfer-Encoding: chunked" << "\r\n";
	  for (auto &h : content_info.extra_headers)
	    os << h << "\r\n";
	  if (!content_info.content_encoding.empty())
	    os << "Content-Encoding: " << content_info.content_encoding << "\r\n";
	  if (content_info.keepalive)
	    os << "Connection: keep-alive\r\n";
	  if (!content_info.lean_headers)
	    os << "Accept: */*\r\n";
	  os << "\r\n";
	}

	void generate_request_websocket(std::ostream& os, const Request& req)
	{
	  os << req.method << ' ' << req.uri << " HTTP/1.1\r\n";
	  os << "Host: " << host.host_head() << "\r\n";
	  if (!config->user_agent.empty())
	    os << "User-Agent: " << config->user_agent << "\r\n";
	  generate_basic_auth_headers(os, req);
	  if (content_info.length)
	  os << "Content-Type: " << content_info.type << "\r\n";
	  if (content_info.websocket)
	    content_info.websocket->client_headers(os);
	  for (auto &h : content_info.extra_headers)
	    os << h << "\r\n";
	  os << "\r\n";
	}

	void generate_basic_auth_headers(std::ostream& os, const Request& req)
	{
	  if (!req.username.empty() || !req.password.empty())
	    os << "Authorization: Basic "
	       << base64->encode(req.username + ':' + req.password)
	       << "\r\n";
	}

	// error handlers

	void asio_error_handler(int errcode, const char *func_name, const openvpn_io::error_code& error)
	{
	  error_handler(errcode, std::string("HTTPCore Asio ") + func_name + ": " + error.message());
	}

	void handle_exception(const char *func_name, const std::exception& e)
	{
	  error_handler(Status::E_EXCEPTION, std::string("HTTPCore Exception ") + func_name + ": " + e.what());
	}

	void error_handler(const int errcode, const std::string& err)
	{
	  const bool in_transaction = !ready;
	  const bool keepalive = alive;
	  const bool error = Status::is_error(errcode);
#if defined(OPENVPN_POLYSOCK_SUPPORTS_ALT_ROUTING)
	  if (config->shim_factory && error && in_transaction && socket)
	    config->shim_factory->report_error(host, socket->alt_routing_enabled());
#endif
	  stop(!error);
	  if (in_transaction)
	    http_done(errcode, err);
	  else if (keepalive)
	    http_keepalive_close(errcode, err); // keepalive connection close outside of transaction
	}

	// methods called by LinkImpl

	bool tcp_read_handler(BufferAllocated& b)
	{
	  if (halt)
	    return false;

	  try {
#ifdef SIMULATE_HTTPCLI_FAILURES // debugging -- simulate network failures
	    if (inject_fault("tcp_read_handler"))
	      return false;
#endif
	    activity(false);
	    tcp_in(b); // call Base
	    return true;
	  }
	  catch (const std::exception& e)
	    {
	      handle_exception("tcp_read_handler", e);
	      return false;
	    }
	}

	void tcp_write_queue_needs_send()
	{
	  if (halt)
	    return;

	  try {
	    http_out();
	  }
	  catch (const std::exception& e)
	    {
	      handle_exception("tcp_write_queue_needs_send", e);
	    }

	}

	void tcp_eof_handler()
	{
	  if (halt)
	    return;

	  try {
	    error_handler(Status::E_EOF_TCP, "TCP EOF");
	    return;
	  }
	  catch (const std::exception& e)
	    {
	      handle_exception("tcp_eof_handler", e);
	    }
	}

	void tcp_error_handler(const char *error)
	{
	  if (halt)
	    return;
	  error_handler(Status::E_TCP, std::string("HTTPCore TCP: ") + error);
	}

	// methods called by Base

	BufferPtr base_http_content_out()
	{
	  return http_content_out();
	}

	void base_http_content_out_needed()
	{
	  if (!content_out_hold)
	    http_content_out_needed();
	}

	void base_http_out_eof()
	{
	  if (websocket)
	    {
	      stop(true);
	      http_done(Status::E_SUCCESS, "Succeeded");
	    }
	}

	bool base_http_headers_received()
	{
	  if (content_info.websocket)
	    websocket = true; // enable websocket in httpcommon
	  http_headers_received();
	  return true;        // continue to receive content
	}

	void base_http_content_in(BufferAllocated& buf)
	{
	  http_content_in(buf);
	}

	bool base_link_send(BufferAllocated& buf)
	{
	  try {
#ifdef SIMULATE_HTTPCLI_FAILURES // debugging -- simulate network failures
	    if (inject_fault("base_link_send"))
	      return false;
#endif
	    activity(false);
	    if (transcli)
	      return transcli->transport_send(buf);
	    else
	      return link->send(buf);
	  }
	  catch (const std::exception& e)
	    {
	      handle_exception("base_link_send", e);
	      return false;
	    }
	}

	bool base_send_queue_empty()
	{
	  if (transcli)
	    return transcli->transport_send_queue_empty();
	  else
	    return link->send_queue_empty();
	}

	void base_http_done_handler(BufferAllocated& residual,
				    const bool parent_handoff)
	{
	  if (halt)
	    return;
	  if ((content_info.keepalive || parent_handoff) && !websocket)
	    {
	      general_timer.cancel();
	      schedule_keepalive_timer();
	      alive = true;
	      ready = true;
	    }
	  else
	    stop(true);
	  http_done(Status::E_SUCCESS, "Succeeded");
	}

	void base_error_handler(const int errcode, const std::string& err)
	{
	  error_handler(errcode, err);
	}

	// TransportClientParent methods

	virtual bool transport_is_openvpn_protocol()
	{
	  return false;
	}

	virtual void transport_recv(BufferAllocated& buf)
	{
	  tcp_read_handler(buf);
	}

	virtual void transport_needs_send()
	{
	  tcp_write_queue_needs_send();
	}

	std::string err_fmt(const Error::Type fatal_err, const std::string& err_text)
	{
	  std::ostringstream os;
	  if (fatal_err != Error::SUCCESS)
	    os << Error::name(fatal_err) << " : ";
	  os << err_text;
	  return os.str();
	}

	virtual void transport_error(const Error::Type fatal_err, const std::string& err_text)
	{
	  return error_handler(Status::E_TRANSPORT, err_fmt(fatal_err, err_text));
	}

	virtual void proxy_error(const Error::Type fatal_err, const std::string& err_text)
	{
	  return error_handler(Status::E_PROXY, err_fmt(fatal_err, err_text));
	}

	virtual void transport_pre_resolve()
	{
	}

	virtual void transport_wait_proxy()
	{
	}

	virtual void transport_wait()
	{
	}

	virtual bool is_keepalive_enabled() const
	{
	  return false;
	}

	virtual void disable_keepalive(unsigned int& keepalive_ping,
				       unsigned int& keepalive_timeout)
	{
	}

	virtual void transport_connecting()
	{
	  do_connect(false);
	}

	openvpn_io::io_context& io_context;

	TimeoutOverride to;

	AsioPolySock::Base::Ptr socket;

#ifndef USE_ASYNC_RESOLVE
	openvpn_io::ip::tcp::resolver resolver;
#endif
	Host host;

	LinkImpl::Ptr link;

	TransportClient::Ptr transcli;

	AsioTimerSafe connect_timer;
	AsioTimerSafe general_timer;
	std::unique_ptr<AsioTimerSafe> req_timer;
	std::unique_ptr<AsioTimerSafe> keepalive_timer;

	Time::Duration general_timeout_duration;
	CoarseTime general_timeout_coarse;

	bool content_out_hold = true;
	bool alive = false;

#ifdef SIMULATE_HTTPCLI_FAILURES // debugging -- simulate network failures
	PeriodicFail periodic_fail;
#endif
      };

      template <typename PARENT>
      class HTTPDelegate : public HTTPCore
      {
      public:
	OPENVPN_EXCEPTION(http_delegate_error);

	typedef RCPtr<HTTPDelegate> Ptr;

	HTTPDelegate(openvpn_io::io_context& io_context,
		     WS::Client::Config::Ptr config,
		     PARENT* parent)
	  : WS::Client::HTTPCore(io_context, std::move(config)),
	    parent_(parent)
	{
	}

	void attach(PARENT* parent)
	{
	  parent_ = parent;
	}

	void detach(const bool keepalive, const bool shutdown)
	{
	  if (parent_)
	    {
	      parent_ = nullptr;
	      if (!keepalive)
		stop(shutdown);
	    }
	}

	PARENT* parent()
	{
	  return parent_;
	}

	virtual Host http_host()
	{
	  if (parent_)
	    return parent_->http_host(*this);
	  else
	    throw http_delegate_error("http_host");
	}

	virtual Request http_request()
	{
	  if (parent_)
	    return parent_->http_request(*this);
	  else
	    throw http_delegate_error("http_request");
	}

	virtual ContentInfo http_content_info()
	{
	  if (parent_)
	    return parent_->http_content_info(*this);
	  else
	    throw http_delegate_error("http_content_info");
	}

	virtual BufferPtr http_content_out()
	{
	  if (parent_)
	    return parent_->http_content_out(*this);
	  else
	    throw http_delegate_error("http_content_out");
	}

	virtual void http_content_out_needed()
	{
	  if (parent_)
	    parent_->http_content_out_needed(*this);
	  else
	    throw http_delegate_error("http_content_out_needed");
	}

	virtual void http_headers_received()
	{
	  if (parent_)
	    parent_->http_headers_received(*this);
	}

	virtual void http_headers_sent(const Buffer& buf)
	{
	  if (parent_)
	    parent_->http_headers_sent(*this, buf);
	}

	virtual void http_mutate_resolver_results(openvpn_io::ip::tcp::resolver::results_type& results)
	{
	  if (parent_)
	    parent_->http_mutate_resolver_results(*this, results);
	}

	virtual void http_content_in(BufferAllocated& buf)
	{
	  if (parent_)
	    parent_->http_content_in(*this, buf);
	}

	virtual void http_done(const int status, const std::string& description)
	{
	  if (parent_)
	    parent_->http_done(*this, status, description);
	}

	virtual void http_keepalive_close(const int status, const std::string& description)
	{
	  if (parent_)
	    parent_->http_keepalive_close(*this, status, description);
	}

	virtual void http_post_connect(AsioPolySock::Base& sock)
	{
	  if (parent_)
	    parent_->http_post_connect(*this, sock);
	}

      private:
	PARENT* parent_;
      };
    }
  }
}