#ifndef OPENVPN_TRANSPORT_CLIENT_HTTPCLI_H
#define OPENVPN_TRANSPORT_CLIENT_HTTPCLI_H

#include <vector>
#include <string>
#include <sstream>
#include <algorithm>                  
#include <memory>

#include <openvpn/io/io.hpp>

#include <openvpn/common/size.hpp>
#include <openvpn/common/exception.hpp>
#include <openvpn/common/string.hpp>
#include <openvpn/common/base64.hpp>
#include <openvpn/common/split.hpp>
#include <openvpn/common/options.hpp>
#include <openvpn/common/number.hpp>
#include <openvpn/common/userpass.hpp>
#include <openvpn/buffer/bufstr.hpp>
#include <openvpn/buffer/buflimit.hpp>
#include <openvpn/transport/tcplink.hpp>
#include <openvpn/transport/client/transbase.hpp>
#include <openvpn/transport/socket_protect.hpp>
#include <openvpn/transport/protocol.hpp>
#include <openvpn/http/reply.hpp>
#include <openvpn/http/status.hpp>
#include <openvpn/http/htmlskip.hpp>
#include <openvpn/proxy/proxyauth.hpp>
#include <openvpn/proxy/httpdigest.hpp>
#include <openvpn/proxy/ntlm.hpp>
#include <openvpn/client/remotelist.hpp>
#include <openvpn/crypto/digestapi.hpp>

namespace openvpn {
  namespace HTTPProxyTransport {

    class Options : public RC<thread_safe_refcount>
    {
    public:
      struct CustomHeader : public RC<thread_unsafe_refcount>
      {
	typedef RCPtr<CustomHeader> Ptr;

	std::string p1;
	std::string p2;
      };

      struct CustomHeaderList : public std::vector<CustomHeader::Ptr>
      {
      };

      typedef RCPtr<Options> Ptr;

      Options() : allow_cleartext_auth(false) {}

      RemoteList::Ptr proxy_server;
      std::string username;
      std::string password;
      bool allow_cleartext_auth;

      std::string http_version;
      std::string user_agent;

      CustomHeaderList headers;

      void set_proxy_server(const std::string& host, const std::string& port)
      {
	proxy_server.reset(new RemoteList(host, port, Protocol(Protocol::TCP), "http proxy port"));
      }

      void proxy_server_set_enable_cache(const bool enable_cache)
      {
	proxy_server->set_enable_cache(enable_cache);
      }

      void proxy_server_precache(RemoteList::Ptr& r)
      {
	if (proxy_server->get_enable_cache())
	  r = proxy_server;
      }

      static Ptr parse(const OptionList& opt)
      {
	if (opt.exists("http-proxy"))
	  {
	    Ptr obj(new Options);
	    if (obj->parse_options(opt))
	      return obj;
	  }
	return Ptr();
      }

    private:
      bool parse_options(const OptionList& opt)
      {
	const Option* hp = opt.get_ptr("http-proxy");
	if (hp)
	  {

	    set_proxy_server(hp->get(1, 256), hp->get(2, 16));

	    {
	      std::vector<std::string> user_pass;
	      if (UserPass::parse(opt, "http-proxy-user-pass", 0, &user_pass))
		{
		  if (user_pass.size() >= 1)
		    username = user_pass[0];
		  if (user_pass.size() >= 2)
		    password = user_pass[1];
		}
	    }

	    allow_cleartext_auth = (hp->get_optional(3, 16) != "auto-nct");

	    const OptionList::IndexList* hpo = opt.get_index_ptr("http-proxy-option");
	    if (hpo)
	      {
		for (OptionList::IndexList::const_iterator i = hpo->begin(); i != hpo->end(); ++i)
		  {
		    const Option& o = opt[*i];
		    const std::string& type = o.get(1, 64);
		    if (type == "VERSION")
		      {
			http_version = o.get(2, 16);
			o.touch();
		      }
		    else if (type == "AGENT")
		      {
			user_agent = o.get(2, 256);
			o.touch();
		      }
		    else if (type == "EXT1" || type == "EXT2" || type == "CUSTOM-HEADER")
		      {
			CustomHeader::Ptr h(new CustomHeader());
			h->p1 = o.get(2, 512);
			h->p2 = o.get_optional(3, 512);
			headers.push_back(h);
			o.touch();
		      }
		  }
	      }
	    return true;
	  }
	else
	  return false;
      }
    };

    class ClientConfig : public TransportClientFactory
    {
    public:
      typedef RCPtr<ClientConfig> Ptr;

      RemoteList::Ptr remote_list;
      size_t free_list_max_size;
      Frame::Ptr frame;
      SessionStats::Ptr stats;

      Options::Ptr http_proxy_options;

      RandomAPI::Ptr rng; 

      DigestFactory::Ptr digest_factory; 

      SocketProtect* socket_protect;

      bool skip_html;

      static Ptr new_obj()
      {
	return new ClientConfig;
      }

      virtual TransportClient::Ptr new_transport_client_obj(openvpn_io::io_context& io_context,
							    TransportClientParent* parent);

    private:
      ClientConfig()
	: free_list_max_size(8),
	  socket_protect(nullptr),
	  skip_html(false)
      {}
    };

    class Client : public TransportClient
    {
      typedef RCPtr<Client> Ptr;

      typedef TCPTransport::Link<openvpn_io::ip::tcp, Client*, false> LinkImpl;

      friend class ClientConfig;                        
      friend LinkImpl;                                  

    public:
      virtual void transport_start()
      {
	if (!impl)
	  {
	    if (!config->http_proxy_options)
	      {
		parent->proxy_error(Error::PROXY_ERROR, "http_proxy_options not defined");
		return;
	      }

	    halt = false;

	    remote_list().endpoint_available(&server_host, &server_port, nullptr);

	    if (proxy_remote_list().endpoint_available(&proxy_host, &proxy_port, nullptr))
	      {

		start_connect_();
	      }
	    else
	      {

		parent->transport_pre_resolve();
		resolver.async_resolve(proxy_host, proxy_port,
				       [self=Ptr(this)](const openvpn_io::error_code& error, openvpn_io::ip::tcp::resolver::results_type results)
				       {
					 self->do_resolve_(error, results);
				       });
	      }
	  }
      }

      virtual bool transport_send_const(const Buffer& buf)
      {
	return send_const(buf);
      }

      virtual bool transport_send(BufferAllocated& buf)
      {
	return send(buf);
      }

      virtual bool transport_send_queue_empty()
      {
	if (impl)
	  return impl->send_queue_empty();
	else
	  return false;
      }

      virtual bool transport_has_send_queue()
      {
	return true;
      }

      virtual void transport_stop_requeueing() { }

      virtual unsigned int transport_send_queue_size()
      {
	if (impl)
	  return impl->send_queue_size();
	else
	  return 0;
      }

      virtual void reset_align_adjust(const size_t align_adjust)
      {
	if (impl)
	  impl->reset_align_adjust(align_adjust);
      }

      virtual void server_endpoint_info(std::string& host, std::string& port, std::string& proto, std::string& ip_addr) const
      {
	host = server_host;
	port = server_port;
	const IP::Addr addr = server_endpoint_addr();
	proto = "TCP";
	proto += addr.version_string();
	proto += "-via-HTTP";
	ip_addr = addr.to_string();
      }

      virtual IP::Addr server_endpoint_addr() const
      {
	return IP::Addr::from_asio(server_endpoint.address());
      }

      virtual Protocol transport_protocol() const
      {
	if (server_endpoint.address().is_v4())
	  return Protocol(Protocol::TCPv4);
	else if (server_endpoint.address().is_v6())
	  return Protocol(Protocol::TCPv6);
	else
	  return Protocol();
      }

      virtual void stop() { stop_(); }
      virtual ~Client() { stop_(); }

    private:
      struct ProxyResponseLimit : public BufferLimit<size_t>
      {
	ProxyResponseLimit() : BufferLimit(1024, 65536) {}

	virtual void bytes_exceeded() {
	  OPENVPN_THROW_EXCEPTION("HTTP proxy response too large (> " << max_bytes << " bytes)");
	}

	virtual void lines_exceeded() {
	  OPENVPN_THROW_EXCEPTION("HTTP proxy response too large (> " << max_lines << " lines)");
	}
      };

      Client(openvpn_io::io_context& io_context_arg,
	     ClientConfig* config_arg,
	     TransportClientParent* parent_arg)
	:  io_context(io_context_arg),
	   socket(io_context_arg),
	   config(config_arg),
	   parent(parent_arg),
	   resolver(io_context_arg),
	   halt(false),
	   n_transactions(0),
	   proxy_established(false),
	   http_reply_status(HTTP::ReplyParser::pending),
	   ntlm_phase_2_response_pending(false),
	   drain_content_length(0)
      {
      }

      virtual void transport_reparent(TransportClientParent* parent_arg)
      {
	parent = parent_arg;
      }

      bool send_const(const Buffer& cbuf)
      {
	if (impl)
	  {
	    BufferAllocated buf(cbuf, 0);
	    return impl->send(buf);
	  }
	else
	  return false;
      }

      bool send(BufferAllocated& buf)
      {
	if (impl)
	  return impl->send(buf);
	else
	  return false;
      }

      void tcp_error_handler(const char *error) 
      {
	std::ostringstream os;
	os << "Transport error on '" << server_host << "' via HTTP proxy " << proxy_host << ':' << proxy_port << " : " << error;
	stop();
	parent->transport_error(Error::TRANSPORT_ERROR, os.str());
      }

      void proxy_error(const Error::Type fatal_err, const std::string& what)
      {
	std::ostringstream os;
	os << "on " << proxy_host << ':' << proxy_port << ": " << what;
	stop();
	parent->proxy_error(fatal_err, os.str());
      }

      bool tcp_read_handler(BufferAllocated& buf) 
      {
	if (proxy_established)
	  {
	    if (!html_skip)
	      parent->transport_recv(buf);
	    else
	      drain_html(buf); 
	  }
	else
	  {
	    try {
	      proxy_read_handler(buf);
	    }
	    catch (const std::exception& e)
	      {
		proxy_error(Error::PROXY_ERROR, e.what());
	      }
	  }
	return true;
      }

      void tcp_write_queue_needs_send() 
      {
	if (proxy_established)
	  parent->transport_needs_send();
      }

      void tcp_eof_handler() 
      {
	if (proxy_established)
	  {
	    config->stats->error(Error::NETWORK_EOF_ERROR);
	    tcp_error_handler("NETWORK_EOF_ERROR");
	  }
	else
	  {
	    try {
	      proxy_eof_handler();
	    }
	    catch (const std::exception& e)
	      {
		proxy_error(Error::PROXY_ERROR, e.what());
	      }
	  }
      }

      void proxy_read_handler(BufferAllocated& buf)
      {

	proxy_response_limit.add(buf);

	if (http_reply_status == HTTP::ReplyParser::pending)
	  {
	    OPENVPN_LOG_NTNL("FROM PROXY: " << buf_to_string(buf));
	    for (size_t i = 0; i < buf.size(); ++i)
	      {
		http_reply_status = http_parser.consume(http_reply, (char)buf[i]);
		if (http_reply_status != HTTP::ReplyParser::pending)
		  {
		    buf.advance(i+1);
		    if (http_reply_status == HTTP::ReplyParser::success)
		      {

			if (http_reply.status_code == HTTP::Status::Connected)
			  {
			    if (config->skip_html)
			      {
				proxy_half_connected();
				html_skip.reset(new HTTP::HTMLSkip());
				drain_html(buf);
			      }
			    else
			      proxy_connected(buf, true);
			  }
			else if (ntlm_phase_2_response_pending)
			  ntlm_auth_phase_2_pre();
		      }
		    else
		      {
			throw Exception("HTTP proxy header parse error");
		      }
		    break;
		  }
	      }
	  }

	if (drain_content_length)
	  {
	    const size_t drain = std::min(drain_content_length, buf.size());
	    buf.advance(drain);
	    drain_content_length -= drain;
	    if (!drain_content_length)
	      {
		if (ntlm_phase_2_response_pending)
		  ntlm_auth_phase_2();
	      }
	  }
      }

      void proxy_connected(BufferAllocated& buf, const bool notify_parent)
      {
	proxy_established = true;
	if (parent->transport_is_openvpn_protocol())
	  {

	    impl->set_raw_mode(false);
	    if (notify_parent)
	      parent->transport_connecting();
	    try {
	      impl->inject(buf);
	    }
	    catch (const std::exception& e)
	      {
		proxy_error(Error::PROXY_ERROR, std::string("post-header inject error: ") + e.what());
		return;
	      }
	  }
	else
	  {
	    if (notify_parent)
	      parent->transport_connecting();
	    parent->transport_recv(buf);
	  }
      }

      void proxy_half_connected()
      {
	proxy_established = true;
	if (parent->transport_is_openvpn_protocol())
	  impl->set_raw_mode_write(false);
	parent->transport_connecting();
      }

      void drain_html(BufferAllocated& buf)
      {
	while (!buf.empty())
	  {
	    switch (html_skip->add(buf.pop_front()))
	      {
	      case HTTP::HTMLSkip::MATCH:
	      case HTTP::HTMLSkip::NOMATCH:
		{
		  OPENVPN_LOG("Proxy: Skipped " << html_skip->n_bytes() << " byte(s) of HTML");
		  html_skip->get_residual(buf);
		  html_skip.reset();
		  proxy_connected(buf, false);
		  return;
		}
	      case HTTP::HTMLSkip::PENDING:
		break;
	      }
	  }
      }

      HTTPProxy::ProxyAuthenticate::Ptr get_proxy_authenticate_header(const char *type)
      {
	for (HTTP::HeaderList::const_iterator i = http_reply.headers.begin(); i != http_reply.headers.end(); ++i)
	  {
	    const HTTP::Header& h = *i;
	    if (string::strcasecmp(h.name, "proxy-authenticate") == 0)
	      {
		HTTPProxy::ProxyAuthenticate::Ptr pa = new HTTPProxy::ProxyAuthenticate(h.value);
		if (string::strcasecmp(type, pa->method) == 0)
		  return pa;
	      }
	  }
	return HTTPProxy::ProxyAuthenticate::Ptr();
      }

      void proxy_eof_handler()
      {
	if (http_reply_status == HTTP::ReplyParser::success)
	  {
	    if (http_reply.status_code == HTTP::Status::ProxyAuthenticationRequired)
	      {
		if (n_transactions <= 1)
		  {

		    if (config->http_proxy_options->username.empty())
		      {
			proxy_error(Error::PROXY_NEED_CREDS, "HTTP proxy requires credentials");
			return;
		      }

		    HTTPProxy::ProxyAuthenticate::Ptr pa;

		    pa = get_proxy_authenticate_header("ntlm");
		    if (pa)
		      {
			ntlm_auth_phase_1(*pa);
			return;
		      }

		    pa = get_proxy_authenticate_header("digest");
		    if (pa)
		      {
			digest_auth(*pa);
			return;
		      }

		    pa = get_proxy_authenticate_header("basic");
		    if (pa)
		      {
			if (config->http_proxy_options->allow_cleartext_auth)
			  {
			    basic_auth(*pa);
			    return;
			  }
			else
			  throw Exception("HTTP proxy Basic authentication not allowed by user preference");
		      }
		    throw Exception("HTTP proxy-authenticate method must be Basic, Digest, or NTLM");
		  }
		else
		  {
		    proxy_error(Error::PROXY_NEED_CREDS, "HTTP proxy credentials were not accepted");
		    return;
		  }
	      }
	    else if (http_reply.status_code == HTTP::Status::ProxyError
		     || http_reply.status_code == HTTP::Status::NotFound
		     || http_reply.status_code == HTTP::Status::ServiceUnavailable)
	      {

		proxy_error(Error::UNDEF, "HTTP proxy server could not connect to OpenVPN server");
		return;
	      }
	    else if (http_reply.status_code == HTTP::Status::Forbidden)
	      OPENVPN_THROW_EXCEPTION("HTTP proxy returned Forbidden status code");
	    else
	      OPENVPN_THROW_EXCEPTION("HTTP proxy status code: " << http_reply.status_code);
	  }
	else if (http_reply_status == HTTP::ReplyParser::pending)
	  throw Exception("HTTP proxy unexpected EOF: reply incomplete");
	else
	  throw Exception("HTTP proxy general error");
      }

      void basic_auth(HTTPProxy::ProxyAuthenticate& pa)
      {
	OPENVPN_LOG("Proxy method: Basic" << std::endl << pa.to_string());

	std::ostringstream os;
	gen_headers(os);
	os << "Proxy-Authorization: Basic "
	   << base64->encode(config->http_proxy_options->username + ':' + config->http_proxy_options->password)
	   << "\r\n";
	http_request = os.str();
	reset();
	start_connect_();
      }

      void digest_auth(HTTPProxy::ProxyAuthenticate& pa)
      {
	try {
	  OPENVPN_LOG("Proxy method: Digest" << std::endl << pa.to_string());

	  const std::string http_method = "CONNECT";
	  const std::string nonce_count = "00000001";
	  const std::string qop = "auth";

	  const std::string realm = pa.parms.get_value("realm");
	  const std::string nonce = pa.parms.get_value("nonce");
	  const std::string algorithm = pa.parms.get_value("algorithm");
	  const std::string opaque = pa.parms.get_value("opaque");

	  unsigned char cnonce_raw[8];
	  config->rng->assert_crypto();
	  config->rng->rand_bytes(cnonce_raw, sizeof(cnonce_raw));
	  const std::string cnonce = render_hex(cnonce_raw, sizeof(cnonce_raw));

	  const std::string uri = server_host + ":" + server_port;

	  const std::string session_key = HTTPProxy::Digest::calcHA1(
	      *config->digest_factory,
	      algorithm,
	      config->http_proxy_options->username,
	      realm,
	      config->http_proxy_options->password,
	      nonce,
	      cnonce);

	  const std::string response = HTTPProxy::Digest::calcResponse(
	      *config->digest_factory,
	      session_key,
	      nonce,
	      nonce_count,
	      cnonce,
	      qop,
	      http_method,
	      uri,
	      "");

	  std::ostringstream os;
	  gen_headers(os);
	  os << "Proxy-Authorization: Digest username=\"" << config->http_proxy_options->username << "\", realm=\"" << realm << "\", nonce=\"" << nonce << "\", uri=\"" << uri << "\", qop=" << qop << ", nc=" << nonce_count << ", cnonce=\"" << cnonce << "\", response=\"" << response << "\"";
	  if (!opaque.empty())
	    os << ", opaque=\"" + opaque + "\"";
	  os << "\r\n";

	  http_request = os.str();
	  reset();
	  start_connect_();
	}
	catch (const std::exception& e)
	  {
	    proxy_error(Error::PROXY_NEED_CREDS, std::string("Digest Auth: ") + e.what());
	  }
      }

      std::string get_ntlm_phase_2_response()
      {
	for (HTTP::HeaderList::const_iterator i = http_reply.headers.begin(); i != http_reply.headers.end(); ++i)
	  {
	    const HTTP::Header& h = *i;
	    if (string::strcasecmp(h.name, "proxy-authenticate") == 0)
	      {
		std::vector<std::string> v = Split::by_space<std::vector<std::string>, StandardLex, SpaceMatch, Split::NullLimit>(h.value);
		if (v.size() >= 2 && string::strcasecmp("ntlm", v[0]) == 0)
		  return v[1];
	      }
	  }
	return "";
      }

      void ntlm_auth_phase_1(HTTPProxy::ProxyAuthenticate& pa)
      {
	OPENVPN_LOG("Proxy method: NTLM" << std::endl << pa.to_string());

	const std::string phase_1_reply = HTTPProxy::NTLM::phase_1();

	std::ostringstream os;
	gen_headers(os);
	os << "Proxy-Connection: Keep-Alive\r\n";
	os << "Proxy-Authorization: NTLM " << phase_1_reply << "\r\n";

	http_request = os.str();
	reset();
	ntlm_phase_2_response_pending = true;
	start_connect_();
      }

      void ntlm_auth_phase_2_pre()
      {

	const std::string content_length_str = http_reply.headers.get_value_trim("content-length");
	const unsigned int content_length = parse_number_throw<unsigned int>(content_length_str, "content-length");
	if (content_length)
	  drain_content_length = content_length;
	else
	  ntlm_auth_phase_2();
      }

      void ntlm_auth_phase_2()
      {
	ntlm_phase_2_response_pending = false;

	if (http_reply.status_code != HTTP::Status::ProxyAuthenticationRequired)
	  throw Exception("NTLM phase-2 status is not ProxyAuthenticationRequired");

	const std::string phase_2_response = get_ntlm_phase_2_response();
	if (!phase_2_response.empty())
	  ntlm_auth_phase_3(phase_2_response);
	else
	  throw Exception("NTLM phase-2 response missing");
      }

      void ntlm_auth_phase_3(const std::string& phase_2_response)
      {

	try {

	  const std::string phase_3_reply = HTTPProxy::NTLM::phase_3(
	      *config->digest_factory,
	      phase_2_response,
	      config->http_proxy_options->username,
	      config->http_proxy_options->password,
	      *config->rng);

	  std::ostringstream os;
	  gen_headers(os);
	  os << "Proxy-Connection: Keep-Alive\r\n";
	  os << "Proxy-Authorization: NTLM " << phase_3_reply << "\r\n";

	  http_request = os.str();
	  reset_partial();
	  http_proxy_send();
	}
	catch (const std::exception& e)
	  {
	    proxy_error(Error::PROXY_NEED_CREDS, std::string("NTLM Auth: ") + e.what());
	  }
      }

      void gen_headers(std::ostringstream& os)
      {
	bool host_header_sent = false;

	{
	  const Options::CustomHeaderList& headers = config->http_proxy_options->headers;
	  for (Options::CustomHeaderList::const_iterator i = headers.begin(); i != headers.end(); ++i)
	    {
	      const Options::CustomHeader& h = **i;
	      if (!h.p2.empty())
		{
		  os << h.p1 << ": " << h.p2 << "\r\n";
		  if (!string::strcasecmp(h.p1, "host"))
		    host_header_sent = true;
		}
	      else
		{
		  os << h.p1 << "\r\n";
		  const std::string h5 = h.p1.substr(0, 5);
		  if (!string::strcasecmp(h5, "host:"))
		    host_header_sent = true;
		}
	    }
	}

	{
	  const std::string& user_agent = config->http_proxy_options->user_agent;
	  if (!user_agent.empty())
	    os << "User-Agent: " << user_agent << "\r\n";
	}

	if (!host_header_sent)
	  os << "Host: " << server_host << "\r\n";
      }

      void stop_()
      {
	if (!halt)
	  {
	    halt = true;
	    if (impl)
	      impl->stop();

	    socket.close();
	    resolver.cancel();
	  }
      }

      void do_resolve_(const openvpn_io::error_code& error,
		       openvpn_io::ip::tcp::resolver::results_type results)
      {
	if (!halt)
	  {
	    if (!error)
	      {

		proxy_remote_list().set_endpoint_range(results);
		start_connect_();
	      }
	    else
	      {
		std::ostringstream os;
		os << "DNS resolve error on '" << proxy_host << "' for TCP (HTTP proxy): " << error.message();
		config->stats->error(Error::RESOLVE_ERROR);
		stop();
		parent->transport_error(Error::UNDEF, os.str());
	      }
	  }
      }

      void reset()
      {
	stop();
	halt = false;
	proxy_response_limit.reset();
	proxy_established = false;
	reset_partial();
      }

      void reset_partial()
      {
	http_reply_status = HTTP::ReplyParser::pending;
	http_reply.reset();
	http_parser.reset();
	ntlm_phase_2_response_pending = false;
	drain_content_length = 0;
	html_skip.reset();
      }

      void start_connect_()
      {
	proxy_remote_list().get_endpoint(server_endpoint);
	OPENVPN_LOG("Contacting " << server_endpoint << " via HTTP Proxy");
	parent->transport_wait_proxy();
	parent->ip_hole_punch(server_endpoint_addr());
	socket.open(server_endpoint.protocol());
#ifdef OPENVPN_PLATFORM_TYPE_UNIX
	if (config->socket_protect)
	  {
	    if (!config->socket_protect->socket_protect(socket.native_handle()))
	      {
		config->stats->error(Error::SOCKET_PROTECT_ERROR);
		stop();
		parent->transport_error(Error::UNDEF, "socket_protect error (HTTP Proxy)");
		return;
	      }
	  }
#endif
	socket.set_option(openvpn_io::ip::tcp::no_delay(true));
	socket.async_connect(server_endpoint, [self=Ptr(this)](const openvpn_io::error_code& error)
                                              {
                                                self->start_impl_(error);
                                              });
      }

      void start_impl_(const openvpn_io::error_code& error)
      {
	if (!halt)
	  {
	    if (!error)
	      {
		parent->transport_wait();
		impl.reset(new LinkImpl(this,
					socket,
					0, 
					config->free_list_max_size,
					(*config->frame)[Frame::READ_LINK_TCP],
					config->stats));
		impl->set_raw_mode(true);
		impl->start();
		++n_transactions;

		http_proxy_send();
	      }
	    else
	      {
		proxy_remote_list().next();

		std::ostringstream os;
		os << "TCP connect error on '" << proxy_host << ':' << proxy_port << "' (" << server_endpoint << ") for TCP-via-HTTP-proxy session: " << error.message();
		config->stats->error(Error::TCP_CONNECT_ERROR);
		stop();
		parent->transport_error(Error::UNDEF, os.str());
	      }
	  }
      }

      void http_proxy_send()
      {
	BufferAllocated buf;
	create_http_connect_msg(buf);
	send(buf);
      }

      void create_http_connect_msg(BufferAllocated& buf)
      {
	std::ostringstream os;
	const std::string& http_version = config->http_proxy_options->http_version;
	os << "CONNECT " << server_host << ':' << server_port << " HTTP/";
	if (!http_version.empty())
	  os << http_version;
	else
	  os << "1.0";
	os << "\r\n";
	if (!http_request.empty())
	  os << http_request;
	else
	  gen_headers(os);
	os << "\r\n";
	const std::string str = os.str();
	http_request = "";

	OPENVPN_LOG_NTNL("TO PROXY: " << str);

	config->frame->prepare(Frame::WRITE_HTTP, buf);
	buf_write_string(buf, str);
      }

      RemoteList& remote_list() const { return *config->remote_list; }
      RemoteList& proxy_remote_list() const { return *config->http_proxy_options->proxy_server; }

      std::string proxy_host;
      std::string proxy_port;

      std::string server_host;
      std::string server_port;

      openvpn_io::io_context& io_context;
      openvpn_io::ip::tcp::socket socket;
      ClientConfig::Ptr config;
      TransportClientParent* parent;
      LinkImpl::Ptr impl;
      openvpn_io::ip::tcp::resolver resolver;
      LinkImpl::protocol::endpoint server_endpoint;
      bool halt;

      unsigned int n_transactions;
      ProxyResponseLimit proxy_response_limit;
      bool proxy_established;
      HTTP::ReplyParser::status http_reply_status;
      HTTP::Reply http_reply;
      HTTP::ReplyParser http_parser;
      std::string http_request;

      bool ntlm_phase_2_response_pending;
      size_t drain_content_length;

      std::unique_ptr<HTTP::HTMLSkip> html_skip;
    };

    inline TransportClient::Ptr ClientConfig::new_transport_client_obj(openvpn_io::io_context& io_context, TransportClientParent* parent)
    {
      return TransportClient::Ptr(new Client(io_context, this, parent));
    }
  }
} 

#endif
//    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-2017 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/>.

// HTTP proxy transport object.

#ifndef OPENVPN_TRANSPORT_CLIENT_HTTPCLI_H
#define OPENVPN_TRANSPORT_CLIENT_HTTPCLI_H

#include <vector>
#include <string>
#include <sstream>
#include <algorithm>                  // for std::min
#include <memory>

#include <openvpn/io/io.hpp>

#include <openvpn/common/size.hpp>
#include <openvpn/common/exception.hpp>
#include <openvpn/common/string.hpp>
#include <openvpn/common/base64.hpp>
#include <openvpn/common/split.hpp>
#include <openvpn/common/options.hpp>
#include <openvpn/common/number.hpp>
#include <openvpn/common/userpass.hpp>
#include <openvpn/buffer/bufstr.hpp>
#include <openvpn/buffer/buflimit.hpp>
#include <openvpn/transport/tcplink.hpp>
#include <openvpn/transport/client/transbase.hpp>
#include <openvpn/transport/socket_protect.hpp>
#include <openvpn/transport/protocol.hpp>
#include <openvpn/http/reply.hpp>
#include <openvpn/http/status.hpp>
#include <openvpn/http/htmlskip.hpp>
#include <openvpn/proxy/proxyauth.hpp>
#include <openvpn/proxy/httpdigest.hpp>
#include <openvpn/proxy/ntlm.hpp>
#include <openvpn/client/remotelist.hpp>
#include <openvpn/crypto/digestapi.hpp>

namespace openvpn {
  namespace HTTPProxyTransport {

    class Options : public RC<thread_safe_refcount>
    {
    public:
      struct CustomHeader : public RC<thread_unsafe_refcount>
      {
	typedef RCPtr<CustomHeader> Ptr;

	std::string p1;
	std::string p2;
      };

      struct CustomHeaderList : public std::vector<CustomHeader::Ptr>
      {
      };

      typedef RCPtr<Options> Ptr;

      Options() : allow_cleartext_auth(false) {}

      RemoteList::Ptr proxy_server;
      std::string username;
      std::string password;
      bool allow_cleartext_auth;

      std::string http_version;
      std::string user_agent;

      CustomHeaderList headers;

      void set_proxy_server(const std::string& host, const std::string& port)
      {
	proxy_server.reset(new RemoteList(host, port, Protocol(Protocol::TCP), "http proxy port"));
      }

      void proxy_server_set_enable_cache(const bool enable_cache)
      {
	proxy_server->set_enable_cache(enable_cache);
      }

      void proxy_server_precache(RemoteList::Ptr& r)
      {
	if (proxy_server->get_enable_cache())
	  r = proxy_server;
      }

      static Ptr parse(const OptionList& opt)
      {
	if (opt.exists("http-proxy"))
	  {
	    Ptr obj(new Options);
	    if (obj->parse_options(opt))
	      return obj;
	  }
	return Ptr();
      }

    private:
      bool parse_options(const OptionList& opt)
      {
	const Option* hp = opt.get_ptr("http-proxy");
	if (hp)
	  {
	    // get server/port
	    set_proxy_server(hp->get(1, 256), hp->get(2, 16));

	    // get creds
	    {
	      std::vector<std::string> user_pass;
	      if (UserPass::parse(opt, "http-proxy-user-pass", 0, &user_pass))
		{
		  if (user_pass.size() >= 1)
		    username = user_pass[0];
		  if (user_pass.size() >= 2)
		    password = user_pass[1];
		}
	    }

	    // allow cleartext auth?
	    allow_cleartext_auth = (hp->get_optional(3, 16) != "auto-nct");

	    // get options
	    const OptionList::IndexList* hpo = opt.get_index_ptr("http-proxy-option");
	    if (hpo)
	      {
		for (OptionList::IndexList::const_iterator i = hpo->begin(); i != hpo->end(); ++i)
		  {
		    const Option& o = opt[*i];
		    const std::string& type = o.get(1, 64);
		    if (type == "VERSION")
		      {
			http_version = o.get(2, 16);
			o.touch();
		      }
		    else if (type == "AGENT")
		      {
			user_agent = o.get(2, 256);
			o.touch();
		      }
		    else if (type == "EXT1" || type == "EXT2" || type == "CUSTOM-HEADER")
		      {
			CustomHeader::Ptr h(new CustomHeader());
			h->p1 = o.get(2, 512);
			h->p2 = o.get_optional(3, 512);
			headers.push_back(h);
			o.touch();
		      }
		  }
	      }
	    return true;
	  }
	else
	  return false;
      }
    };

    class ClientConfig : public TransportClientFactory
    {
    public:
      typedef RCPtr<ClientConfig> Ptr;

      RemoteList::Ptr remote_list;
      size_t free_list_max_size;
      Frame::Ptr frame;
      SessionStats::Ptr stats;

      Options::Ptr http_proxy_options;

      RandomAPI::Ptr rng; // random data source

      DigestFactory::Ptr digest_factory; // needed by proxy auth methods

      SocketProtect* socket_protect;

      bool skip_html;

      static Ptr new_obj()
      {
	return new ClientConfig;
      }

      virtual TransportClient::Ptr new_transport_client_obj(openvpn_io::io_context& io_context,
							    TransportClientParent* parent);

    private:
      ClientConfig()
	: free_list_max_size(8),
	  socket_protect(nullptr),
	  skip_html(false)
      {}
    };

    class Client : public TransportClient
    {
      typedef RCPtr<Client> Ptr;

      typedef TCPTransport::Link<openvpn_io::ip::tcp, Client*, false> LinkImpl;

      friend class ClientConfig;                        // calls constructor
      friend LinkImpl;                                  // calls tcp_read_handler

    public:
      virtual void transport_start()
      {
	if (!impl)
	  {
	    if (!config->http_proxy_options)
	      {
		parent->proxy_error(Error::PROXY_ERROR, "http_proxy_options not defined");
		return;
	      }

	    halt = false;

	    // Get target server host:port.  We don't care about resolving it
	    // since proxy server will do that for us.
	    remote_list().endpoint_available(&server_host, &server_port, nullptr);

	    // Get proxy server host:port, and resolve it if not already cached
	    if (proxy_remote_list().endpoint_available(&proxy_host, &proxy_port, nullptr))
	      {
		// already cached
		start_connect_();
	      }
	    else
	      {
		// resolve it
		parent->transport_pre_resolve();
		resolver.async_resolve(proxy_host, proxy_port,
				       [self=Ptr(this)](const openvpn_io::error_code& error, openvpn_io::ip::tcp::resolver::results_type results)
				       {
					 self->do_resolve_(error, results);
				       });
	      }
	  }
      }

      virtual bool transport_send_const(const Buffer& buf)
      {
	return send_const(buf);
      }

      virtual bool transport_send(BufferAllocated& buf)
      {
	return send(buf);
      }

      virtual bool transport_send_queue_empty()
      {
	if (impl)
	  return impl->send_queue_empty();
	else
	  return false;
      }

      virtual bool transport_has_send_queue()
      {
	return true;
      }

      virtual void transport_stop_requeueing() { }

      virtual unsigned int transport_send_queue_size()
      {
	if (impl)
	  return impl->send_queue_size();
	else
	  return 0;
      }

      virtual void reset_align_adjust(const size_t align_adjust)
      {
	if (impl)
	  impl->reset_align_adjust(align_adjust);
      }

      virtual void server_endpoint_info(std::string& host, std::string& port, std::string& proto, std::string& ip_addr) const
      {
	host = server_host;
	port = server_port;
	const IP::Addr addr = server_endpoint_addr();
	proto = "TCP";
	proto += addr.version_string();
	proto += "-via-HTTP";
	ip_addr = addr.to_string();
      }

      virtual IP::Addr server_endpoint_addr() const
      {
	return IP::Addr::from_asio(server_endpoint.address());
      }

      virtual Protocol transport_protocol() const
      {
	if (server_endpoint.address().is_v4())
	  return Protocol(Protocol::TCPv4);
	else if (server_endpoint.address().is_v6())
	  return Protocol(Protocol::TCPv6);
	else
	  return Protocol();
      }

      virtual void stop() { stop_(); }
      virtual ~Client() { stop_(); }

    private:
      struct ProxyResponseLimit : public BufferLimit<size_t>
      {
	ProxyResponseLimit() : BufferLimit(1024, 65536) {}

	virtual void bytes_exceeded() {
	  OPENVPN_THROW_EXCEPTION("HTTP proxy response too large (> " << max_bytes << " bytes)");
	}

	virtual void lines_exceeded() {
	  OPENVPN_THROW_EXCEPTION("HTTP proxy response too large (> " << max_lines << " lines)");
	}
      };

      Client(openvpn_io::io_context& io_context_arg,
	     ClientConfig* config_arg,
	     TransportClientParent* parent_arg)
	:  io_context(io_context_arg),
	   socket(io_context_arg),
	   config(config_arg),
	   parent(parent_arg),
	   resolver(io_context_arg),
	   halt(false),
	   n_transactions(0),
	   proxy_established(false),
	   http_reply_status(HTTP::ReplyParser::pending),
	   ntlm_phase_2_response_pending(false),
	   drain_content_length(0)
      {
      }

      virtual void transport_reparent(TransportClientParent* parent_arg)
      {
	parent = parent_arg;
      }

      bool send_const(const Buffer& cbuf)
      {
	if (impl)
	  {
	    BufferAllocated buf(cbuf, 0);
	    return impl->send(buf);
	  }
	else
	  return false;
      }

      bool send(BufferAllocated& buf)
      {
	if (impl)
	  return impl->send(buf);
	else
	  return false;
      }

      void tcp_error_handler(const char *error) // called by LinkImpl and internally
      {
	std::ostringstream os;
	os << "Transport error on '" << server_host << "' via HTTP proxy " << proxy_host << ':' << proxy_port << " : " << error;
	stop();
	parent->transport_error(Error::TRANSPORT_ERROR, os.str());
      }

      void proxy_error(const Error::Type fatal_err, const std::string& what)
      {
	std::ostringstream os;
	os << "on " << proxy_host << ':' << proxy_port << ": " << what;
	stop();
	parent->proxy_error(fatal_err, os.str());
      }

      bool tcp_read_handler(BufferAllocated& buf) // called by LinkImpl
      {
	if (proxy_established)
	  {
	    if (!html_skip)
	      parent->transport_recv(buf);
	    else
	      drain_html(buf); // skip extraneous HTML after header
	  }
	else
	  {
	    try {
	      proxy_read_handler(buf);
	    }
	    catch (const std::exception& e)
	      {
		proxy_error(Error::PROXY_ERROR, e.what());
	      }
	  }
	return true;
      }

      void tcp_write_queue_needs_send() // called by LinkImpl
      {
	if (proxy_established)
	  parent->transport_needs_send();
      }

      void tcp_eof_handler() // called by LinkImpl
      {
	if (proxy_established)
	  {
	    config->stats->error(Error::NETWORK_EOF_ERROR);
	    tcp_error_handler("NETWORK_EOF_ERROR");
	  }
	else
	  {
	    try {
	      proxy_eof_handler();
	    }
	    catch (const std::exception& e)
	      {
		proxy_error(Error::PROXY_ERROR, e.what());
	      }
	  }
      }

      void proxy_read_handler(BufferAllocated& buf)
      {
	// for anti-DoS, only allow a maximum number of chars in HTTP response
	proxy_response_limit.add(buf);

	if (http_reply_status == HTTP::ReplyParser::pending)
	  {
	    OPENVPN_LOG_NTNL("FROM PROXY: " << buf_to_string(buf));
	    for (size_t i = 0; i < buf.size(); ++i)
	      {
		http_reply_status = http_parser.consume(http_reply, (char)buf[i]);
		if (http_reply_status != HTTP::ReplyParser::pending)
		  {
		    buf.advance(i+1);
		    if (http_reply_status == HTTP::ReplyParser::success)
		      {
			//OPENVPN_LOG("*** HTTP header parse complete, resid_size=" << buf.size());
			//OPENVPN_LOG(http_reply.to_string());

			// we are connected, switch socket to tunnel mode
			if (http_reply.status_code == HTTP::Status::Connected)
			  {
			    if (config->skip_html)
			      {
				proxy_half_connected();
				html_skip.reset(new HTTP::HTMLSkip());
				drain_html(buf);
			      }
			    else
			      proxy_connected(buf, true);
			  }
			else if (ntlm_phase_2_response_pending)
			  ntlm_auth_phase_2_pre();
		      }
		    else
		      {
			throw Exception("HTTP proxy header parse error");
		      }
		    break;
		  }
	      }
	  }

	// handle draining of content controlled by Content-length header
	if (drain_content_length)
	  {
	    const size_t drain = std::min(drain_content_length, buf.size());
	    buf.advance(drain);
	    drain_content_length -= drain;
	    if (!drain_content_length)
	      {
		if (ntlm_phase_2_response_pending)
		  ntlm_auth_phase_2();
	      }
	  }
      }

      void proxy_connected(BufferAllocated& buf, const bool notify_parent)
      {
	proxy_established = true;
	if (parent->transport_is_openvpn_protocol())
	  {
	    // switch socket from HTTP proxy handshake mode to OpenVPN protocol mode
	    impl->set_raw_mode(false);
	    if (notify_parent)
	      parent->transport_connecting();
	    try {
	      impl->inject(buf);
	    }
	    catch (const std::exception& e)
	      {
		proxy_error(Error::PROXY_ERROR, std::string("post-header inject error: ") + e.what());
		return;
	      }
	  }
	else
	  {
	    if (notify_parent)
	      parent->transport_connecting();
	    parent->transport_recv(buf);
	  }
      }

      // Called after header received but before possible extraneous HTML
      // is drained.  At this point, we are in a state where output data
      // (if OpenVPN protocol) is packetized, but input data is still in
      // raw mode as we search the input stream for the end of the
      // extraneous HTML.  When we reach the beginning of payload data,
      // proxy_connected() should be called with notify_parent == false.
      void proxy_half_connected()
      {
	proxy_established = true;
	if (parent->transport_is_openvpn_protocol())
	  impl->set_raw_mode_write(false);
	parent->transport_connecting();
      }

      void drain_html(BufferAllocated& buf)
      {
	while (!buf.empty())
	  {
	    switch (html_skip->add(buf.pop_front()))
	      {
	      case HTTP::HTMLSkip::MATCH:
	      case HTTP::HTMLSkip::NOMATCH:
		{
		  OPENVPN_LOG("Proxy: Skipped " << html_skip->n_bytes() << " byte(s) of HTML");
		  html_skip->get_residual(buf);
		  html_skip.reset();
		  proxy_connected(buf, false);
		  return;
		}
	      case HTTP::HTMLSkip::PENDING:
		break;
	      }
	  }
      }

      HTTPProxy::ProxyAuthenticate::Ptr get_proxy_authenticate_header(const char *type)
      {
	for (HTTP::HeaderList::const_iterator i = http_reply.headers.begin(); i != http_reply.headers.end(); ++i)
	  {
	    const HTTP::Header& h = *i;
	    if (string::strcasecmp(h.name, "proxy-authenticate") == 0)
	      {
		HTTPProxy::ProxyAuthenticate::Ptr pa = new HTTPProxy::ProxyAuthenticate(h.value);
		if (string::strcasecmp(type, pa->method) == 0)
		  return pa;
	      }
	  }
	return HTTPProxy::ProxyAuthenticate::Ptr();
      }

      void proxy_eof_handler()
      {
	if (http_reply_status == HTTP::ReplyParser::success)
	  {
	    if (http_reply.status_code == HTTP::Status::ProxyAuthenticationRequired)
	      {
		if (n_transactions <= 1)
		  {
		    //OPENVPN_LOG("*** PROXY AUTHENTICATION REQUIRED");

		    if (config->http_proxy_options->username.empty())
		      {
			proxy_error(Error::PROXY_NEED_CREDS, "HTTP proxy requires credentials");
			return;
		      }

		    HTTPProxy::ProxyAuthenticate::Ptr pa;

		    // NTLM
		    pa = get_proxy_authenticate_header("ntlm");
		    if (pa)
		      {
			ntlm_auth_phase_1(*pa);
			return;
		      }

		    // Digest
		    pa = get_proxy_authenticate_header("digest");
		    if (pa)
		      {
			digest_auth(*pa);
			return;
		      }

		    // Basic
		    pa = get_proxy_authenticate_header("basic");
		    if (pa)
		      {
			if (config->http_proxy_options->allow_cleartext_auth)
			  {
			    basic_auth(*pa);
			    return;
			  }
			else
			  throw Exception("HTTP proxy Basic authentication not allowed by user preference");
		      }
		    throw Exception("HTTP proxy-authenticate method must be Basic, Digest, or NTLM");
		  }
		else
		  {
		    proxy_error(Error::PROXY_NEED_CREDS, "HTTP proxy credentials were not accepted");
		    return;
		  }
	      }
	    else if (http_reply.status_code == HTTP::Status::ProxyError
		     || http_reply.status_code == HTTP::Status::NotFound
		     || http_reply.status_code == HTTP::Status::ServiceUnavailable)
	      {
		// this is a nonfatal error, so we pass Error::UNDEF to tell the upper layer to
		// retry the connection
		proxy_error(Error::UNDEF, "HTTP proxy server could not connect to OpenVPN server");
		return;
	      }
	    else if (http_reply.status_code == HTTP::Status::Forbidden)
	      OPENVPN_THROW_EXCEPTION("HTTP proxy returned Forbidden status code");
	    else
	      OPENVPN_THROW_EXCEPTION("HTTP proxy status code: " << http_reply.status_code);
	  }
	else if (http_reply_status == HTTP::ReplyParser::pending)
	  throw Exception("HTTP proxy unexpected EOF: reply incomplete");
	else
	  throw Exception("HTTP proxy general error");
      }

      void basic_auth(HTTPProxy::ProxyAuthenticate& pa)
      {
	OPENVPN_LOG("Proxy method: Basic" << std::endl << pa.to_string());

	std::ostringstream os;
	gen_headers(os);
	os << "Proxy-Authorization: Basic "
	   << base64->encode(config->http_proxy_options->username + ':' + config->http_proxy_options->password)
	   << "\r\n";
	http_request = os.str();
	reset();
	start_connect_();
      }

      void digest_auth(HTTPProxy::ProxyAuthenticate& pa)
      {
	try {
	  OPENVPN_LOG("Proxy method: Digest" << std::endl << pa.to_string());

	  // constants
	  const std::string http_method = "CONNECT";
	  const std::string nonce_count = "00000001";
	  const std::string qop = "auth";

	  // get values from Proxy-Authenticate header
	  const std::string realm = pa.parms.get_value("realm");
	  const std::string nonce = pa.parms.get_value("nonce");
	  const std::string algorithm = pa.parms.get_value("algorithm");
	  const std::string opaque = pa.parms.get_value("opaque");

	  // generate a client nonce
	  unsigned char cnonce_raw[8];
	  config->rng->assert_crypto();
	  config->rng->rand_bytes(cnonce_raw, sizeof(cnonce_raw));
	  const std::string cnonce = render_hex(cnonce_raw, sizeof(cnonce_raw));

	  // build URI
	  const std::string uri = server_host + ":" + server_port;

	  // calculate session key
	  const std::string session_key = HTTPProxy::Digest::calcHA1(
	      *config->digest_factory,
	      algorithm,
	      config->http_proxy_options->username,
	      realm,
	      config->http_proxy_options->password,
	      nonce,
	      cnonce);

	  // calculate response
	  const std::string response = HTTPProxy::Digest::calcResponse(
	      *config->digest_factory,
	      session_key,
	      nonce,
	      nonce_count,
	      cnonce,
	      qop,
	      http_method,
	      uri,
	      "");

	  // generate proxy request
	  std::ostringstream os;
	  gen_headers(os);
	  os << "Proxy-Authorization: Digest username=\"" << config->http_proxy_options->username << "\", realm=\"" << realm << "\", nonce=\"" << nonce << "\", uri=\"" << uri << "\", qop=" << qop << ", nc=" << nonce_count << ", cnonce=\"" << cnonce << "\", response=\"" << response << "\"";
	  if (!opaque.empty())
	    os << ", opaque=\"" + opaque + "\"";
	  os << "\r\n";

	  http_request = os.str();
	  reset();
	  start_connect_();
	}
	catch (const std::exception& e)
	  {
	    proxy_error(Error::PROXY_NEED_CREDS, std::string("Digest Auth: ") + e.what());
	  }
      }

      std::string get_ntlm_phase_2_response()
      {
	for (HTTP::HeaderList::const_iterator i = http_reply.headers.begin(); i != http_reply.headers.end(); ++i)
	  {
	    const HTTP::Header& h = *i;
	    if (string::strcasecmp(h.name, "proxy-authenticate") == 0)
	      {
		std::vector<std::string> v = Split::by_space<std::vector<std::string>, StandardLex, SpaceMatch, Split::NullLimit>(h.value);
		if (v.size() >= 2 && string::strcasecmp("ntlm", v[0]) == 0)
		  return v[1];
	      }
	  }
	return "";
      }

      void ntlm_auth_phase_1(HTTPProxy::ProxyAuthenticate& pa)
      {
	OPENVPN_LOG("Proxy method: NTLM" << std::endl << pa.to_string());

	const std::string phase_1_reply = HTTPProxy::NTLM::phase_1();

	std::ostringstream os;
	gen_headers(os);
	os << "Proxy-Connection: Keep-Alive\r\n";
	os << "Proxy-Authorization: NTLM " << phase_1_reply << "\r\n";

	http_request = os.str();
	reset();
	ntlm_phase_2_response_pending = true;
	start_connect_();
      }

      void ntlm_auth_phase_2_pre()
      {
	// if content exists, drain it first, then progress to ntlm_auth_phase_2
	const std::string content_length_str = http_reply.headers.get_value_trim("content-length");
	const unsigned int content_length = parse_number_throw<unsigned int>(content_length_str, "content-length");
	if (content_length)
	  drain_content_length = content_length;
	else
	  ntlm_auth_phase_2();
      }

      void ntlm_auth_phase_2()
      {
	ntlm_phase_2_response_pending = false;

	if (http_reply.status_code != HTTP::Status::ProxyAuthenticationRequired)
	  throw Exception("NTLM phase-2 status is not ProxyAuthenticationRequired");

	const std::string phase_2_response = get_ntlm_phase_2_response();
	if (!phase_2_response.empty())
	  ntlm_auth_phase_3(phase_2_response);
	else
	  throw Exception("NTLM phase-2 response missing");
      }

      void ntlm_auth_phase_3(const std::string& phase_2_response)
      {
	// do the NTLMv2 handshake
	try {
	  //OPENVPN_LOG("NTLM phase 3: " << phase_2_response);

	  const std::string phase_3_reply = HTTPProxy::NTLM::phase_3(
	      *config->digest_factory,
	      phase_2_response,
	      config->http_proxy_options->username,
	      config->http_proxy_options->password,
	      *config->rng);

	  std::ostringstream os;
	  gen_headers(os);
	  os << "Proxy-Connection: Keep-Alive\r\n";
	  os << "Proxy-Authorization: NTLM " << phase_3_reply << "\r\n";

	  http_request = os.str();
	  reset_partial();
	  http_proxy_send();
	}
	catch (const std::exception& e)
	  {
	    proxy_error(Error::PROXY_NEED_CREDS, std::string("NTLM Auth: ") + e.what());
	  }
      }

      void gen_headers(std::ostringstream& os)
      {
	bool host_header_sent = false;

	// emit custom headers
	{
	  const Options::CustomHeaderList& headers = config->http_proxy_options->headers;
	  for (Options::CustomHeaderList::const_iterator i = headers.begin(); i != headers.end(); ++i)
	    {
	      const Options::CustomHeader& h = **i;
	      if (!h.p2.empty())
		{
		  os << h.p1 << ": " << h.p2 << "\r\n";
		  if (!string::strcasecmp(h.p1, "host"))
		    host_header_sent = true;
		}
	      else
		{
		  os << h.p1 << "\r\n";
		  const std::string h5 = h.p1.substr(0, 5);
		  if (!string::strcasecmp(h5, "host:"))
		    host_header_sent = true;
		}
	    }
	}

	// emit user-agent header
	{
	  const std::string& user_agent = config->http_proxy_options->user_agent;
	  if (!user_agent.empty())
	    os << "User-Agent: " << user_agent << "\r\n";
	}

	// emit host header
	if (!host_header_sent)
	  os << "Host: " << server_host << "\r\n";
      }

      void stop_()
      {
	if (!halt)
	  {
	    halt = true;
	    if (impl)
	      impl->stop();

	    socket.close();
	    resolver.cancel();
	  }
      }

      // do DNS resolve
      void do_resolve_(const openvpn_io::error_code& error,
		       openvpn_io::ip::tcp::resolver::results_type results)
      {
	if (!halt)
	  {
	    if (!error)
	      {
		// save resolved endpoint list in proxy remote_list
		proxy_remote_list().set_endpoint_range(results);
		start_connect_();
	      }
	    else
	      {
		std::ostringstream os;
		os << "DNS resolve error on '" << proxy_host << "' for TCP (HTTP proxy): " << error.message();
		config->stats->error(Error::RESOLVE_ERROR);
		stop();
		parent->transport_error(Error::UNDEF, os.str());
	      }
	  }
      }

      void reset()
      {
	stop();
	halt = false;
	proxy_response_limit.reset();
	proxy_established = false;
	reset_partial();
      }

      void reset_partial()
      {
	http_reply_status = HTTP::ReplyParser::pending;
	http_reply.reset();
	http_parser.reset();
	ntlm_phase_2_response_pending = false;
	drain_content_length = 0;
	html_skip.reset();
      }

      // do TCP connect
      void start_connect_()
      {
	proxy_remote_list().get_endpoint(server_endpoint);
	OPENVPN_LOG("Contacting " << server_endpoint << " via HTTP Proxy");
	parent->transport_wait_proxy();
	parent->ip_hole_punch(server_endpoint_addr());
	socket.open(server_endpoint.protocol());
#ifdef OPENVPN_PLATFORM_TYPE_UNIX
	if (config->socket_protect)
	  {
	    if (!config->socket_protect->socket_protect(socket.native_handle()))
	      {
		config->stats->error(Error::SOCKET_PROTECT_ERROR);
		stop();
		parent->transport_error(Error::UNDEF, "socket_protect error (HTTP Proxy)");
		return;
	      }
	  }
#endif
	socket.set_option(openvpn_io::ip::tcp::no_delay(true));
	socket.async_connect(server_endpoint, [self=Ptr(this)](const openvpn_io::error_code& error)
                                              {
                                                self->start_impl_(error);
                                              });
      }

      // start I/O on TCP socket
      void start_impl_(const openvpn_io::error_code& error)
      {
	if (!halt)
	  {
	    if (!error)
	      {
		parent->transport_wait();
		impl.reset(new LinkImpl(this,
					socket,
					0, // send_queue_max_size is unlimited because we regulate size in cliproto.hpp
					config->free_list_max_size,
					(*config->frame)[Frame::READ_LINK_TCP],
					config->stats));
		impl->set_raw_mode(true);
		impl->start();
		++n_transactions;

		// tell proxy to connect through to OpenVPN server
		http_proxy_send();
	      }
	    else
	      {
		proxy_remote_list().next();

		std::ostringstream os;
		os << "TCP connect error on '" << proxy_host << ':' << proxy_port << "' (" << server_endpoint << ") for TCP-via-HTTP-proxy session: " << error.message();
		config->stats->error(Error::TCP_CONNECT_ERROR);
		stop();
		parent->transport_error(Error::UNDEF, os.str());
	      }
	  }
      }

      void http_proxy_send()
      {
	BufferAllocated buf;
	create_http_connect_msg(buf);
	send(buf);
      }

      // create HTTP CONNECT message
      void create_http_connect_msg(BufferAllocated& buf)
      {
	std::ostringstream os;
	const std::string& http_version = config->http_proxy_options->http_version;
	os << "CONNECT " << server_host << ':' << server_port << " HTTP/";
	if (!http_version.empty())
	  os << http_version;
	else
	  os << "1.0";
	os << "\r\n";
	if (!http_request.empty())
	  os << http_request;
	else
	  gen_headers(os);
	os << "\r\n";
	const std::string str = os.str();
	http_request = "";

	OPENVPN_LOG_NTNL("TO PROXY: " << str);

	config->frame->prepare(Frame::WRITE_HTTP, buf);
	buf_write_string(buf, str);
      }

      RemoteList& remote_list() const { return *config->remote_list; }
      RemoteList& proxy_remote_list() const { return *config->http_proxy_options->proxy_server; }

      std::string proxy_host;
      std::string proxy_port;

      std::string server_host;
      std::string server_port;

      openvpn_io::io_context& io_context;
      openvpn_io::ip::tcp::socket socket;
      ClientConfig::Ptr config;
      TransportClientParent* parent;
      LinkImpl::Ptr impl;
      openvpn_io::ip::tcp::resolver resolver;
      LinkImpl::protocol::endpoint server_endpoint;
      bool halt;

      unsigned int n_transactions;
      ProxyResponseLimit proxy_response_limit;
      bool proxy_established;
      HTTP::ReplyParser::status http_reply_status;
      HTTP::Reply http_reply;
      HTTP::ReplyParser http_parser;
      std::string http_request;

      bool ntlm_phase_2_response_pending;
      size_t drain_content_length;

      std::unique_ptr<HTTP::HTMLSkip> html_skip;
    };

    inline TransportClient::Ptr ClientConfig::new_transport_client_obj(openvpn_io::io_context& io_context, TransportClientParent* parent)
    {
      return TransportClient::Ptr(new Client(io_context, this, parent));
    }
  }
} // namespace openvpn

#endif
