/*
* Copyright (c) 2013-2026, The PurpleI2P Project
*
* This file is part of Purple i2pd project and licensed under BSD3
*
* See full license text in LICENSE file at top of project tree
*/

#include <cstring>
#include <cassert>
#include <string>
#include <string_view>
#include <atomic>
#include <memory>
#include <set>
#include <boost/asio.hpp>
#include <boost/algorithm/string/predicate.hpp>
#include <mutex>

#include "I2PService.h"
#include "Destination.h"
#include "HTTPProxy.h"
#include "util.h"
#include "Identity.h"
#include "Streaming.h"
#include "Destination.h"
#include "ClientContext.h"
#include "I2PEndian.h"
#include "I2PTunnel.h"
#include "Config.h"
#include "HTTP.h"
#include "I18N.h"
#include "Socks5.h"

namespace i2p
{
namespace proxy
{
	static constexpr std::array jumporder =
	{
		"reg.i2p",
		"stats.i2p",
		"identiguy.i2p",
		"notbob.i2p"
	};

	static const std::map<std::string, std::string> jumpservices = {
		{ "reg.i2p",       "http://shx5vqsw7usdaunyzr2qmes2fq37oumybpudrd4jjj4e4vk4uusa.b32.i2p/jump/" },
		{ "identiguy.i2p", "http://3mzmrus2oron5fxptw7hw2puho3bnqmw2hqy7nw64dsrrjwdilva.b32.i2p/cgi-bin/query?hostname=" },
		{ "stats.i2p",     "http://7tbay5p4kzeekxvyvbf6v7eauazemsnnl2aoyqhg5jzpr5eke7tq.b32.i2p/cgi-bin/jump.cgi?a=" },
		{ "notbob.i2p",    "http://nytzrhrjjfsutowojvxi7hphesskpqqr65wpistz6wa7cpajhp7a.b32.i2p/cgi-bin/jump.cgi?q=" }
	};

	static constexpr std::string_view pageHead =
		"<head>\r\n"
		"  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\r\n"
		"  <title>I2Pd HTTP proxy</title>\r\n"
		"  <style type=\"text/css\">\r\n"
		"    body { font: 100%/1.5em sans-serif; margin: 0; padding: 1.5em; background: #FAFAFA; color: #103456; }\r\n"
		"    h1 { font-size: 1.7em; color: #894C84; }\r\n"
		"    @media screen and (max-width: 980px) { h1 { font-size: 1.7em; text-align: center; color: #894C84; }}\r\n"
		"  </style>\r\n"
		"</head>\r\n"
	;

	static bool str_rmatch(std::string_view str, std::string_view suffix)
	{
		auto pos = str.rfind (suffix);
		if (pos == std::string_view::npos)
			return false; /* not found */
		if (str.length() == pos + suffix.length ())
			return true; /* match */
		return false;
	}

	class HTTPReqHandler: public i2p::client::I2PServiceHandler, public std::enable_shared_from_this<HTTPReqHandler>
	{
		private:

			bool HandleRequest();
			void HandleSockRecv(const boost::system::error_code & ecode, std::size_t bytes_transfered);
			void Terminate();
			void AsyncSockRead();
			static bool ExtractAddressHelper(i2p::http::URL& url, std::string& jump, bool& confirm);
			static bool VerifyAddressHelper (std::string_view jump);
			void SanitizeHTTPRequest(i2p::http::HTTPReq& req);
			void SentHTTPFailed(const boost::system::error_code & ecode);
			void HandleStreamRequestComplete (std::shared_ptr<i2p::stream::Stream> stream);
			/* error helpers */
			void GenericProxyError(std::string_view title, std::string_view description);
			void GenericProxyInfo(std::string_view title, std::string_view description);
			void HostNotFound(std::string_view host);
			void SendProxyError(std::string_view content);
			void SendRedirect(const std::string& address);

			void ForwardToUpstreamProxy();
			void HandleUpstreamHTTPProxyConnect(const boost::system::error_code & ec);
			void HandleUpstreamSocksProxyConnect(const boost::system::error_code & ec);
			void HTTPConnect(std::string_view host, uint16_t port);
			void HandleHTTPConnectStreamRequestComplete(std::shared_ptr<i2p::stream::Stream> stream);

			typedef std::function<void(boost::asio::ip::tcp::endpoint)> ProxyResolvedHandler;

			void HandleUpstreamProxyResolved(const boost::system::error_code & ecode, boost::asio::ip::tcp::resolver::results_type endpoints, ProxyResolvedHandler handler);

			void SocksProxySuccess();
			void HandoverToUpstreamProxy();

			uint8_t m_recv_chunk[8192];
			std::string m_recv_buf; // from client
			std::string m_send_buf; // to upstream
			std::shared_ptr<boost::asio::ip::tcp::socket> m_sock;
			std::shared_ptr<boost::asio::ip::tcp::socket> m_proxysock;
			boost::asio::ip::tcp::resolver m_proxy_resolver;
			std::string m_OutproxyUrl, m_Response;
			bool m_Addresshelper, m_SendUserAgent;
			i2p::http::URL m_ProxyURL;
			i2p::http::URL m_RequestURL;
			int m_req_len;
			i2p::http::URL m_ClientRequestURL;
			i2p::http::HTTPReq m_ClientRequest;
			i2p::http::HTTPRes m_ClientResponse;
			std::stringstream m_ClientRequestBuffer;

		public:

			HTTPReqHandler(HTTPProxy * parent, std::shared_ptr<boost::asio::ip::tcp::socket> sock) :
				I2PServiceHandler(parent), m_sock(sock),
				m_proxysock(std::make_shared<boost::asio::ip::tcp::socket>(parent->GetService())),
				m_proxy_resolver(parent->GetService()),
				m_OutproxyUrl(parent->GetOutproxyURL()),
				m_Addresshelper(parent->GetHelperSupport()),
				m_SendUserAgent (parent->GetSendUserAgent ()) {}
			~HTTPReqHandler() { Terminate(); }
			void Handle () { AsyncSockRead(); } /* overload */
	};

	void HTTPReqHandler::AsyncSockRead()
	{
		LogPrint(eLogDebug, "HTTPProxy: Async sock read");
		if (!m_sock) {
			LogPrint(eLogError, "HTTPProxy: No socket for read");
			return;
		}
		m_sock->async_read_some(boost::asio::buffer(m_recv_chunk, sizeof(m_recv_chunk)),
			std::bind(&HTTPReqHandler::HandleSockRecv, shared_from_this(),
			std::placeholders::_1, std::placeholders::_2));
	}

	void HTTPReqHandler::Terminate() {
		if (Kill()) return;
		if (m_sock)
		{
			LogPrint(eLogDebug, "HTTPProxy: Close sock");
			m_sock->close();
			m_sock = nullptr;
		}
		if(m_proxysock)
		{
			LogPrint(eLogDebug, "HTTPProxy: Close proxysock");
			if(m_proxysock->is_open())
				m_proxysock->close();
			m_proxysock = nullptr;
		}
		Done(shared_from_this());
	}

	void HTTPReqHandler::GenericProxyError(std::string_view title, std::string_view description)
	{
		std::stringstream ss;
		ss << "<h1>" << tr("Proxy error") << ": " << title << "</h1>\r\n";
		ss << "<p>" << description << "</p>\r\n";
		SendProxyError(ss.str ());
	}

	void HTTPReqHandler::GenericProxyInfo(std::string_view title, std::string_view description)
	{
		std::stringstream ss;
		ss << "<h1>" << tr("Proxy info") << ": " << title << "</h1>\r\n";
		ss << "<p>" << description << "</p>\r\n";
		SendProxyError(ss.str ());
	}

	void HTTPReqHandler::HostNotFound(std::string_view host)
	{
		std::stringstream ss;
		ss << "<h1>" << tr("Proxy error: Host not found") << "</h1>\r\n"
		   << "<p>" << tr("Remote host not found in router's addressbook") << "</p>\r\n"
		   << "<p>" << tr("You may try to find this host on jump services below") << ":</p>\r\n"
		   << "<ul>\r\n";
		for (const auto& jump : jumporder)
		{
			auto js = jumpservices.find (jump);
			if (js != jumpservices.end())
				ss << "  <li><a href=\"" << js->second << host << "\">" << js->first << "</a></li>\r\n";
		}
		ss << "</ul>\r\n";
		SendProxyError(ss.str ());
	}

	void HTTPReqHandler::SendProxyError(std::string_view content)
	{
		i2p::http::HTTPRes res;
		res.code = 500;
		res.add_header("Content-Type", "text/html; charset=UTF-8");
		res.add_header("Connection", "close");
		std::stringstream ss;
		ss << "<html>\r\n" << pageHead
		   << "<body>" << content << "</body>\r\n"
		   << "</html>\r\n";
		res.body = ss.str();
		m_Response = res.to_string();
		boost::asio::async_write(*m_sock, boost::asio::buffer(m_Response), boost::asio::transfer_all(),
			std::bind(&HTTPReqHandler::SentHTTPFailed, shared_from_this(), std::placeholders::_1));
	}

	void HTTPReqHandler::SendRedirect(const std::string& address)
	{
		i2p::http::HTTPRes res;
		res.code = 302;
		res.add_header("Location", address);
		res.add_header("Connection", "close");
		m_Response = res.to_string();
		boost::asio::async_write(*m_sock, boost::asio::buffer(m_Response), boost::asio::transfer_all(),
			std::bind(&HTTPReqHandler::SentHTTPFailed, shared_from_this(), std::placeholders::_1));
	}

	bool HTTPReqHandler::ExtractAddressHelper(i2p::http::URL& url, std::string& jump, bool& confirm)
	{
		confirm = false;
		const char *param = "i2paddresshelper=";
		std::size_t pos = url.query.find(param);
		std::size_t len = std::strlen(param);
		std::map<std::string, std::string> params;


		if (pos == std::string::npos)
			return false; /* not found */
		if (!url.parse_query(params))
			return false;

		std::string value = params["i2paddresshelper"];
		len += value.length();
		jump = i2p::http::UrlDecode(value);
		if (!VerifyAddressHelper (jump))
		{
			LogPrint (eLogError, "HTTPProxy: Malformed jump link ", jump);
			return false;
		}

		// if we need update exists, request formed with update param
		if (params["update"] == "true")
		{
			len += std::strlen("&update=true");
			confirm = true;
		}

		// if helper is not only one query option and it placed after user's query
		if (pos != 0 && url.query[pos-1] == '&')
		{
			pos--;
			len++;
		}
		// if helper is not only one query option and it placed before user's query
		else if (pos == 0 && url.query.length () > len && url.query[len] == '&')
		{
			// we don't touch the '?' but remove the trailing '&'
			len++;
		}
		else
		{
			// there is no more query options, resetting hasquery flag
			url.hasquery = false;
		}

		// reset hasquery flag and remove addresshelper from URL
		url.query.replace(pos, len, "");
		return true;
	}

	bool HTTPReqHandler::VerifyAddressHelper (std::string_view jump)
	{
		auto pos = jump.find(".b32.i2p");
		if (pos != std::string::npos)
		{
			auto b32 = jump.substr (0, pos);
			for (auto& ch: b32)
				if (!i2p::data::IsBase32(ch)) return false;
			return true;
		}
		else
		{
			bool padding = false;
			for (auto& ch: jump)
			{
				if (ch == '=')
					padding = true;
				else
				{
					if (padding) return false; // other chars after padding
					if (!i2p::data::IsBase64(ch)) return false;
				}
			}
			return true;
		}
		return false;
	}

	void HTTPReqHandler::SanitizeHTTPRequest(i2p::http::HTTPReq& req)
	{
		/* drop common headers */
		req.RemoveHeader("Via");
		req.RemoveHeader("From");
		req.RemoveHeader("Forwarded");
		req.RemoveHeader("DNT"); // Useless DoNotTrack flag
		req.RemoveHeader("Accept", "Accept-Encoding"); // Accept*, but Accept-Encoding
		/* drop proxy-disclosing headers */
		req.RemoveHeader("X-Forwarded");
		req.RemoveHeader("Proxy-"); // Proxy-*
		/* replace headers */
		if (!m_SendUserAgent)
			req.UpdateHeader("User-Agent", "MYOB/6.66 (AN/ON)");

		/**
		 * i2pd PR #1816:
		 * Android Webview send this with the value set to the application ID, so we drop it,
		 * but only if it does not belong to an AJAX request (*HttpRequest, like XMLHttpRequest).
		 */
		if(req.GetHeader("X-Requested-With") != "") {
			auto h = req.GetHeader ("X-Requested-With");
			auto x = h.find("HttpRequest");
			if (x == std::string::npos) // not found
				req.RemoveHeader("X-Requested-With");
		}

		/**
		 * according to i2p ticket #1862:
		 * leave Referer if requested URL with same schema, host and port,
		 * otherwise, drop it.
		 */
		if(req.GetHeader("Referer") != "") {
			i2p::http::URL reqURL; reqURL.parse(req.uri);
			i2p::http::URL refURL; refURL.parse(req.GetHeader("Referer"));
			if(!boost::iequals(reqURL.schema, refURL.schema) || !boost::iequals(reqURL.host, refURL.host) || reqURL.port != refURL.port)
				req.RemoveHeader("Referer");
		}

		/* add headers */
		/* close connection, if not Connection: (U|u)pgrade (for websocket) */
		auto h = req.GetHeader ("Connection");
		auto x = h.find("pgrade");
		if (!(x != std::string::npos && std::tolower(h[x - 1]) == 'u'))
			req.UpdateHeader("Connection", "close");
	}

	/**
	 * @brief Try to parse request from @a m_recv_buf
	 *   If parsing success, rebuild request and store to @a m_send_buf
	 * with remaining data tail
	 * @return true on processed request or false if more data needed
	 */
	bool HTTPReqHandler::HandleRequest()
	{
		m_req_len = m_ClientRequest.parse(m_recv_buf);

		if (m_req_len == 0)
			return false; /* need more data */

		if (m_req_len < 0) {
			LogPrint(eLogError, "HTTPProxy: Unable to parse request");
			GenericProxyError(tr("Invalid request"), tr("Proxy unable to parse your request"));
			return true; /* parse error */
		}

		/* parsing success, now let's look inside request */
		LogPrint(eLogDebug, "HTTPProxy: Requested: ", m_ClientRequest.uri);
		m_RequestURL.parse(m_ClientRequest.uri);
		bool m_Confirm;

		std::string jump;
		if (ExtractAddressHelper(m_RequestURL, jump, m_Confirm))
		{
			if (!m_Addresshelper || !i2p::client::context.GetAddressBook ().IsEnabled ())
			{
				LogPrint(eLogWarning, "HTTPProxy: Addresshelper request rejected");
				GenericProxyError(tr("Invalid request"), tr("Addresshelper is not supported"));
				return true;
			}

			if (i2p::client::context.GetAddressBook ().RecordExists (m_RequestURL.host, jump))
			{
				std::string full_url = m_RequestURL.to_string();
				SendRedirect(full_url);
				return true;
			}
			else if (!i2p::client::context.GetAddressBook ().FindAddress (m_RequestURL.host) || m_Confirm)
			{
				const std::string referer_raw = m_ClientRequest.GetHeader("Referer");
				i2p::http::URL referer_url;
				if (!referer_raw.empty ())
				{
					referer_url.parse (referer_raw);
				}
				if (m_RequestURL.host != referer_url.host)
				{
					if (m_Confirm) // Attempt to forced overwriting by link with "&update=true" from harmful URL
					{
						LogPrint (eLogWarning, "HTTPProxy: Address update from addresshelper rejected for ", m_RequestURL.host, " (referer is ", m_RequestURL.host.empty() ? "empty" : "harmful", ")");
						std::string full_url = m_RequestURL.to_string();
						std::stringstream ss;
						ss << tr("Host %s is <font color=red>already in router's addressbook</font>. <b>Be careful: source of this URL may be harmful!</b> Click here to update record: <a href=\"%s%s%s&update=true\">Continue</a>.",
							m_RequestURL.host.c_str(), full_url.c_str(), (full_url.find('?') != std::string::npos ? "&i2paddresshelper=" : "?i2paddresshelper="), jump.c_str());
						GenericProxyInfo(tr("Addresshelper forced update rejected"), ss.str());
					}
					else // Preventing unauthorized additions to the address book
					{
						LogPrint (eLogDebug, "HTTPProxy: Adding address from addresshelper for ", m_RequestURL.host, " (generate refer-base page)");
						std::string full_url = m_RequestURL.to_string();
						std::stringstream ss;
						ss << tr("To add host <b>%s</b> in router's addressbook, click here: <a href=\"%s%s%s\">Continue</a>.",
							m_RequestURL.host.c_str(), full_url.c_str(), (full_url.find('?') != std::string::npos ? "&i2paddresshelper=" : "?i2paddresshelper="), jump.c_str());
						GenericProxyInfo(tr("Addresshelper request"), ss.str());
					}
					return true; /* request processed */
				}

				i2p::client::context.GetAddressBook ().InsertAddress (m_RequestURL.host, jump);
				LogPrint (eLogInfo, "HTTPProxy: Added address from addresshelper for ", m_RequestURL.host);
				std::string full_url = m_RequestURL.to_string();
				std::stringstream ss;
				ss << tr("Host %s added to router's addressbook from helper. Click here to proceed: <a href=\"%s\">Continue</a>.",
					m_RequestURL.host.c_str(), full_url.c_str());
				GenericProxyInfo(tr("Addresshelper adding"), ss.str());
				return true; /* request processed */
			}
			else
			{
				std::string full_url = m_RequestURL.to_string();
				std::stringstream ss;
				ss << tr("Host %s is <font color=red>already in router's addressbook</font>. Click here to update record: <a href=\"%s%s%s&update=true\">Continue</a>.",
					m_RequestURL.host.c_str(), full_url.c_str(), (full_url.find('?') != std::string::npos ? "&i2paddresshelper=" : "?i2paddresshelper="), jump.c_str());
				GenericProxyInfo(tr("Addresshelper update"), ss.str());
				return true; /* request processed */
			}
		}
		std::string dest_host;
		uint16_t    dest_port;
		bool useConnect = false;
		if(m_ClientRequest.method == "CONNECT")
		{
			const std::string& uri = m_ClientRequest.uri;
			auto pos = uri.find(":");
			if(pos == std::string::npos || pos == uri.size() - 1)
			{
				GenericProxyError(tr("Invalid request"), tr("Invalid request URI"));
				return true;
			}
			else
			{
				useConnect = true;
				dest_port = std::stoi(uri.substr(pos+1));
				dest_host = uri.substr(0, pos);
			}
		}
		else
		{
			SanitizeHTTPRequest(m_ClientRequest);

			dest_host = m_RequestURL.host;
			dest_port = m_RequestURL.port;
			/* always set port, even if missing in request */
			if (!dest_port)
				dest_port = (m_RequestURL.schema == "https") ? 443 : 80;
			/* detect dest_host, set proper 'Host' header in upstream request */
			if (dest_host != "")
			{
				/* absolute url, replace 'Host' header */
				std::string h (dest_host);
				if (dest_port != 0 && dest_port != 80)
					h += ":" + std::to_string(dest_port);
				m_ClientRequest.UpdateHeader("Host", h);
			}
			else
			{
				auto h = m_ClientRequest.GetHeader ("Host");
				if (h.length () > 0)
				{
					/* relative url and 'Host' header provided. transparent proxy mode? */
					i2p::http::URL u;
					std::string t = "http://" + h;
					u.parse(t);
					dest_host = u.host;
					dest_port = u.port;
				}
				else
				{
					/* relative url and missing 'Host' header */
					GenericProxyError(tr("Invalid request"), tr("Can't detect destination host from request"));
					return true;
				}
			}
		}
		/* check dest_host really exists and inside I2P network */
		if (str_rmatch(dest_host, ".i2p"))
		{
			auto addr = i2p::client::context.GetAddressBook ().GetAddress (dest_host);
			if (addr)
			{
				if (!str_rmatch(dest_host, ".b32.i2p"))
				{
					auto b32Host = addr->ToBase32 ();
					if (!b32Host.empty ())
					{
						b32Host += ".b32.i2p";
						if (m_RequestURL.port) b32Host += ":" + std::to_string(m_RequestURL.port);
						m_ClientRequest.UpdateHeader("Host", b32Host); // replace our name to .b32.i2p address
					}
					else
					{
						// invalid address
						HostNotFound(dest_host);
						return true;
					}
				}
			}
			else
			{
				HostNotFound(dest_host);
				return true; /* request processed */
			}
		}
		else
		{
			if(m_OutproxyUrl.size())
			{
				LogPrint (eLogDebug, "HTTPProxy: Using outproxy ", m_OutproxyUrl);
				if(m_ProxyURL.parse(m_OutproxyUrl))
					ForwardToUpstreamProxy();
				else
					GenericProxyError(tr("Outproxy failure"), tr("Bad outproxy settings"));
			}
			else
			{
				LogPrint (eLogWarning, "HTTPProxy: Outproxy failure for ", dest_host, ": no outproxy enabled");
				std::stringstream ss; ss << tr("Host %s is not inside I2P network, but outproxy is not enabled", dest_host.c_str ());
				GenericProxyError(tr("Outproxy failure"), ss.str());
			}
			return true;
		}
		if(useConnect)
		{
			HTTPConnect(dest_host, dest_port);
			return true;
		}

		/* make relative url */
		m_RequestURL.schema = "";
		m_RequestURL.host   = "";
		m_ClientRequest.uri = m_RequestURL.to_string();

		/* drop original request from recv buffer */
		m_recv_buf.erase(0, m_req_len);
		/* build new buffer from modified request and data from original request */
		m_send_buf = m_ClientRequest.to_string();
		m_send_buf.append(m_recv_buf);
		/* connect to destination */
		LogPrint(eLogDebug, "HTTPProxy: Connecting to host ", dest_host, ":", dest_port);
		GetOwner ()->UpdateLastActivityTime ();
		GetOwner()->CreateStream (std::bind (&HTTPReqHandler::HandleStreamRequestComplete,
			shared_from_this(), std::placeholders::_1), dest_host, dest_port);
		return true;
	}

	void HTTPReqHandler::ForwardToUpstreamProxy()
	{
		LogPrint(eLogDebug, "HTTPProxy: Forwarded to upstream");

		/* build http request */
		m_ClientRequestURL = m_RequestURL;
		LogPrint(eLogDebug, "HTTPProxy: ", m_ClientRequestURL.host);
		m_ClientRequestURL.schema = "";
		m_ClientRequestURL.host   = "";
		std::string origURI = m_ClientRequest.uri; // TODO: what do we need to change uri for?
		m_ClientRequest.uri = m_ClientRequestURL.to_string();

		/* update User-Agent to ESR version of Firefox, same as Tor Browser below version 13, for non-HTTPS connections */
		if(m_ClientRequest.method != "CONNECT" && !m_SendUserAgent)
			m_ClientRequest.UpdateHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; rv:109.0) Gecko/20100101 Firefox/115.0");

		m_ClientRequest.write(m_ClientRequestBuffer);
		m_ClientRequestBuffer << m_recv_buf.substr(m_req_len);

		/* assume http if empty schema */
		if (m_ProxyURL.schema == "" || m_ProxyURL.schema == "http")
		{
			/* handle upstream http proxy */
			if (!m_ProxyURL.port) m_ProxyURL.port = 80;
			if (m_ProxyURL.is_i2p())
			{
				m_ClientRequest.uri = origURI;
				auto auth = i2p::http::CreateBasicAuthorizationString (m_ProxyURL.user, m_ProxyURL.pass);
				if (!auth.empty ())
				{
					/* remove existing authorization if any */
					m_ClientRequest.RemoveHeader("Proxy-");
					/* add own http proxy authorization */
					m_ClientRequest.AddHeader("Proxy-Authorization", auth);
				}
				m_send_buf = m_ClientRequest.to_string();
				m_recv_buf.erase(0, m_req_len);
				m_send_buf.append(m_recv_buf);
				GetOwner()->CreateStream (std::bind (&HTTPReqHandler::HandleStreamRequestComplete,
					shared_from_this(), std::placeholders::_1), m_ProxyURL.host, m_ProxyURL.port);
			}
			else
			{
				m_proxy_resolver.async_resolve(m_ProxyURL.host, std::to_string(m_ProxyURL.port), std::bind(&HTTPReqHandler::HandleUpstreamProxyResolved, this,
				    std::placeholders::_1, std::placeholders::_2, [&](boost::asio::ip::tcp::endpoint ep)
				    {
					 	m_proxysock->async_connect(ep, std::bind(&HTTPReqHandler::HandleUpstreamHTTPProxyConnect, this, std::placeholders::_1));
					}));
			}
		}
		else if (m_ProxyURL.schema == "socks")
		{
			/* handle upstream socks proxy */
			if (!m_ProxyURL.port) m_ProxyURL.port = 9050; // default to tor default if not specified
			m_proxy_resolver.async_resolve(m_ProxyURL.host, std::to_string(m_ProxyURL.port), std::bind(&HTTPReqHandler::HandleUpstreamProxyResolved, this,
			    std::placeholders::_1, std::placeholders::_2, [&](boost::asio::ip::tcp::endpoint ep)
			    {
					m_proxysock->async_connect(ep, std::bind(&HTTPReqHandler::HandleUpstreamSocksProxyConnect, this, std::placeholders::_1));
				}));
		}
		else
		{
			/* unknown type, complain */
			GenericProxyError(tr("Unknown outproxy URL"), m_ProxyURL.to_string());
		}
	}

	void HTTPReqHandler::HandleUpstreamProxyResolved(const boost::system::error_code & ec, boost::asio::ip::tcp::resolver::results_type endpoints, ProxyResolvedHandler handler)
	{
		if(ec) GenericProxyError(tr("Cannot resolve upstream proxy"), ec.message());
		else handler(*endpoints.begin ());
	}

	void HTTPReqHandler::HandleUpstreamSocksProxyConnect(const boost::system::error_code & ec)
	{
		if(!ec)
		{
			if(m_RequestURL.host.size() > 255)
			{
				GenericProxyError(tr("Hostname is too long"), m_RequestURL.host);
				return;
			}
			uint16_t port = m_RequestURL.port;
			if(!port) port = 80;
			LogPrint(eLogDebug, "HTTPProxy: Connected to SOCKS upstream");
			std::string host = m_RequestURL.host;
			auto s = shared_from_this ();
			i2p::transport::Socks5Handshake (*m_proxysock, std::make_pair(host, port),
				[s](const boost::system::error_code& ec)
			    {
					if (!ec)
						s->SocksProxySuccess();
					else
						s->GenericProxyError(tr("SOCKS proxy error"), ec.message ());
				});

		}
		else
			GenericProxyError(tr("Cannot connect to upstream SOCKS proxy"), ec.message());
	}

	void HTTPReqHandler::HandoverToUpstreamProxy()
	{
		LogPrint(eLogDebug, "HTTPProxy: Handover to SOCKS proxy");
		auto connection = CreateSocketsPipe (GetOwner(), m_proxysock, m_sock);
		m_sock = nullptr;
		m_proxysock = nullptr;
		GetOwner()->AddHandler(connection);
		connection->Start();
		Terminate();
	}

	void HTTPReqHandler::HTTPConnect(std::string_view host, uint16_t port)
	{
		LogPrint(eLogDebug, "HTTPProxy: CONNECT ",host, ":", port);
		if(str_rmatch(host, ".i2p"))
			GetOwner()->CreateStream (std::bind (&HTTPReqHandler::HandleHTTPConnectStreamRequestComplete,
				shared_from_this(), std::placeholders::_1), host, port);
		else
			ForwardToUpstreamProxy();
	}

	void HTTPReqHandler::HandleHTTPConnectStreamRequestComplete(std::shared_ptr<i2p::stream::Stream> stream)
	{
		if(stream)
		{
			if (m_sock && m_sock->is_open ())
			{
				m_ClientResponse.code = 200;
				m_ClientResponse.status = "OK";
				m_send_buf = m_ClientResponse.to_string();
				m_sock->send(boost::asio::buffer(m_send_buf));
				auto connection = std::make_shared<i2p::client::I2PTunnelConnection>(GetOwner(), m_sock, stream);
				GetOwner()->AddHandler(connection);
				connection->I2PConnect();
			}
			else
				stream->AsyncClose ();
			m_sock = nullptr;
			Terminate();
		}
		else
		{
			GenericProxyError(tr("CONNECT error"), tr("Failed to connect"));
		}
	}

	void HTTPReqHandler::SocksProxySuccess()
	{
		if(m_ClientRequest.method == "CONNECT") {
			m_ClientResponse.code = 200;
			m_send_buf = m_ClientResponse.to_string();
			boost::asio::async_write(*m_sock, boost::asio::buffer(m_send_buf), boost::asio::transfer_all(), [&] (const boost::system::error_code & ec, std::size_t transferred)
				{
					if(ec) GenericProxyError(tr("SOCKS proxy error"), ec.message());
					else HandoverToUpstreamProxy();
				});
		} else {
			m_send_buf = m_ClientRequestBuffer.str();
			LogPrint(eLogDebug, "HTTPProxy: Send ", m_send_buf.size(), " bytes");
			boost::asio::async_write(*m_proxysock, boost::asio::buffer(m_send_buf), boost::asio::transfer_all(), [&](const boost::system::error_code & ec, std::size_t transferred)
				{
					if(ec) GenericProxyError(tr("Failed to send request to upstream"), ec.message());
					else HandoverToUpstreamProxy();
				});
		}
	}

	void HTTPReqHandler::HandleUpstreamHTTPProxyConnect(const boost::system::error_code & ec)
	{
		if(!ec) {
			LogPrint(eLogDebug, "HTTPProxy: Connected to http upstream");
			GenericProxyError(tr("Cannot connect"), tr("HTTP out proxy not implemented"));
		} else GenericProxyError(tr("Cannot connect to upstream HTTP proxy"), ec.message());
	}

	/* will be called after some data received from client */
	void HTTPReqHandler::HandleSockRecv(const boost::system::error_code & ecode, std::size_t len)
	{
		LogPrint(eLogDebug, "HTTPProxy: Sock recv: ", len, " bytes, recv buf: ", m_recv_buf.length(), ", send buf: ", m_send_buf.length());
		if(ecode)
		{
			LogPrint(eLogWarning, "HTTPProxy: Sock recv got error: ", ecode);
			Terminate();
			return;
		}

		m_recv_buf.append(reinterpret_cast<const char *>(m_recv_chunk), len);
		if (HandleRequest()) {
			m_recv_buf.clear();
			return;
		}
		AsyncSockRead();
	}

	void HTTPReqHandler::SentHTTPFailed(const boost::system::error_code & ecode)
	{
		if (ecode)
			LogPrint (eLogError, "HTTPProxy: Closing socket after sending failure because: ", ecode.message ());
		Terminate();
	}

	void HTTPReqHandler::HandleStreamRequestComplete (std::shared_ptr<i2p::stream::Stream> stream)
	{
		if (!stream) {
			LogPrint (eLogError, "HTTPProxy: Error when creating the stream, check the previous warnings for more info");
			GenericProxyError(tr("Host is down"), tr("Can't create connection to requested host, it may be down. Please try again later."));
			return;
		}
		if (Kill())
			return;
		LogPrint (eLogDebug, "HTTPProxy: Created new I2PTunnel stream, sSID=", stream->GetSendStreamID(), ", rSID=", stream->GetRecvStreamID());
		auto connection = std::make_shared<i2p::client::I2PClientTunnelConnectionHTTP>(GetOwner(), m_sock, stream);
		GetOwner()->AddHandler (connection);
		connection->I2PConnect (reinterpret_cast<const uint8_t*>(m_send_buf.data()), m_send_buf.length());
		Done (shared_from_this());
	}

	HTTPProxy::HTTPProxy(const std::string& name, const std::string& address, uint16_t port,
	    const std::string & outproxy, bool addresshelper, bool senduseragent, std::shared_ptr<i2p::client::ClientDestination> localDestination):
		TCPIPAcceptor (address, port, localDestination ? localDestination : i2p::client::context.GetSharedLocalDestination ()),
		m_Name (name), m_OutproxyUrl (outproxy), m_Addresshelper (addresshelper), m_SendUserAgent (senduseragent)
	{
	}

	std::shared_ptr<i2p::client::I2PServiceHandler> HTTPProxy::CreateHandler(std::shared_ptr<boost::asio::ip::tcp::socket> socket)
	{
		return std::make_shared<HTTPReqHandler> (this, socket);
	}
} // http
} // i2p
