//
// This file is part of the aMule Project.
//
// Copyright (c) 2003-2026 aMule Team ( https://amule-org.github.io )
// Copyright (c) 2004-2011 Angel Vidal ( kry@amule.org )
//
// Any parts of this program derived from the xMule, lMule or eMule project,
// or contributed by third-party developers are copyrighted by their
// respective authors.
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301, USA
//

#include "RemoteConnect.h"

#include <common/SmartPtr.h>	// Needed for CSmartPtr
#include <common/MD5Sum.h>
#include <common/Format.h>
#include "../../../amuleIPV4Address.h"
#include "../../../NetworkFunctions.h"	// IsLoopbackIP / IsLanIP / IsLinkLocalIP / StringIPtoUint32

#include <wx/intl.h>
#include <common/StringFunctions.h>	// unicode2char for stderr message
#ifdef __WINDOWS__
	#include <process.h>		// _exit
#else
	#include <unistd.h>		// _exit
#endif

wxDEFINE_EVENT(wxEVT_EC_CONNECTION, wxEvent);
CECLoginPacket::CECLoginPacket(const wxString& client, const wxString& version,
							   bool canZLIB, bool canUTF8numbers, bool canNotify,
							   bool preferNoZlib)
:
CECPacket(EC_OP_AUTH_REQ)
{
	AddTag(CECTag(EC_TAG_CLIENT_NAME, client));
	AddTag(CECTag(EC_TAG_CLIENT_VERSION, version));
	AddTag(CECTag(EC_TAG_PROTOCOL_VERSION, (uint64)EC_CURRENT_PROTOCOL_VERSION));

	#ifdef EC_VERSION_ID
	CMD4Hash versionhash;
	wxCHECK2(versionhash.Decode(EC_VERSION_ID), /* Do nothing. */);
	AddTag(CECTag(EC_TAG_VERSION_ID, versionhash));
	#endif

	// Send capabilities:
	// support ZLIB compression
	if (canZLIB)		AddTag(CECEmptyTag(EC_TAG_CAN_ZLIB));
	// support encoding of integers as UTF-8
	if (canUTF8numbers) AddTag(CECEmptyTag(EC_TAG_CAN_UTF8_NUMBERS));
	// client accepts push messages
	if (canNotify)		AddTag(CECEmptyTag(EC_TAG_CAN_NOTIFY));
	// client can decode the sentinel-extended children-count wire format
	// from CECTag::WriteChildren (#199). Always advertised by new
	// clients; old servers ignore the unknown tag.
	AddTag(CECEmptyTag(EC_TAG_CAN_LARGE_TAG_COUNT));
	// Client implements the partial-update INC_UPDATE protocol — server
	// may skip unchanged files and signal deletions explicitly via
	// `EC_TAG_FILE_REMOVED` instead of relying on absence-implies-
	// deletion. Always advertised by new clients; old servers ignore
	// the unknown tag and the server falls back to emitting alive-
	// marker tags for unchanged files (still backward-compatible).
	AddTag(CECEmptyTag(EC_TAG_CAN_PARTIAL_UPDATE));
	// Client tells the server "we believe transit between us is fast
	// (loopback / LAN), so skip per-packet ZLIB up to the receiver
	// gate". The server honours this hint at WritePacket time; see
	// ECSocket.cpp `m_isLocalPeer`. The decision lives on the client
	// because only the client knows the IP it dialed — server-side
	// peer-IP inspection would misclassify e.g. WireGuard tunnel
	// endpoints as "local" when the underlying transit is anything but.
	// Old servers ignore the unknown tag and fall back to always-zlib.
	if (preferNoZlib)	AddTag(CECEmptyTag(EC_TAG_PREFER_NO_ZLIB));
}

CECAuthPacket::CECAuthPacket(const wxString& pass)
:
CECPacket(EC_OP_AUTH_PASSWD)
{
	CMD4Hash passhash;
	wxCHECK2(passhash.Decode(pass), /* Do nothing. */);
	AddTag(CECTag(EC_TAG_PASSWD_HASH, passhash));
}

/*!
 * Connection to remote core
 *
 */

CRemoteConnect::CRemoteConnect(wxEvtHandler* evt_handler)
:
CECMuleSocket(evt_handler != 0),
m_ec_state(EC_INIT),
m_req_fifo(),
// Give application some indication about how fast requests are served
// When request fifo contain more that certain number of entries, it may
// indicate that either core or network is slowing us down
m_req_count(0),
// This is not mean to be absolute limit, because we can't drop requests
// out of calling context; it is just signal to application to slow down
m_req_fifo_thr(20),
m_notifier(evt_handler),
m_canZLIB(false),
m_canUTF8numbers(false),
m_canNotify(false),
m_preferNoZlib(false),
m_forceZlib(false),
m_serverPartialUpdate(false)
{
}

void CRemoteConnect::SetCapabilities(bool canZLIB, bool canUTF8numbers, bool canNotify)
{
	m_canZLIB = canZLIB;
	if (canZLIB) {
		m_my_flags |= EC_FLAG_ZLIB;
	}
	m_canUTF8numbers = canUTF8numbers;
	if (canUTF8numbers) {
		m_my_flags |= EC_FLAG_UTF8_NUMBERS;
	}
	m_canNotify = canNotify;
}

bool CRemoteConnect::ConnectToCore(const wxString &host, int port,
	const wxString &WXUNUSED(login), const wxString &pass,
	const wxString& client, const wxString& version)
{
	m_connectionPassword = pass;

	m_client = client;
	m_version = version;

	// don't even try to connect without a valid password
	if (m_connectionPassword.IsEmpty() || m_connectionPassword == "d41d8cd98f00b204e9800998ecf8427e") {
		m_server_reply = _("You must specify a non-empty password.");
		return false;
	} else {
		CMD4Hash hash;
		if (!hash.Decode(m_connectionPassword)) {
			m_server_reply = _("Invalid password, not a MD5 hash!");
			return false;
		} else if (hash.IsEmpty()) {
			m_server_reply = _("You must specify a non-empty password.");
			return false;
		}
	}

	amuleIPV4Address addr;

	addr.Hostname(host);
	addr.Service(port);

	// Compute the prefer-no-ZLIB hint after host resolution: if we
	// dialed a loopback / RFC1918 LAN / RFC3927 link-local IP, the
	// transit is fast and per-packet ZLIB is pure overhead. Skip the
	// check entirely if the user opted out of ZLIB via /EC/ZLIB=0
	// (capability isn't advertised so the hint is moot) or set
	// `/EC/ForceZLIB=1` / `--force-zlib` to handle e.g. a WireGuard
	// tunnel endpoint that resolves to an RFC1918 IP but whose
	// underlying transit is slow Internet.
	m_preferNoZlib = false;
	if (m_canZLIB && !m_forceZlib) {
		uint32 resolved_ip = 0;
		if (StringIPtoUint32(addr.IPAddress(), resolved_ip)) {
			m_preferNoZlib = IsLoopbackIP(resolved_ip)
				|| IsLanIP(resolved_ip)
				|| IsLinkLocalIP(resolved_ip);
		}
	}

	if (ConnectSocket(addr)) {
		// We get here only in case of synchronous connect.
		// Otherwise we continue in OnConnect.
		CECLoginPacket login_req(m_client, m_version, m_canZLIB, m_canUTF8numbers, m_canNotify, m_preferNoZlib);

		CSmartPtr<const CECPacket> getSalt(SendRecvPacket(&login_req));
		m_ec_state = EC_REQ_SENT;

		ProcessAuthPacket(getSalt.get());

		CECAuthPacket passwdPacket(m_connectionPassword);

		CSmartPtr<const CECPacket> reply(SendRecvPacket(&passwdPacket));
		m_ec_state = EC_PASSWD_SENT;

		return ProcessAuthPacket(reply.get());
	} else if (m_notifier) {
		m_ec_state = EC_CONNECT_SENT;
	} else {
		return false;
	}

	return true;
}

bool CRemoteConnect::IsConnectedToLocalHost()
{
	amuleIPV4Address addr;
	return addr.Hostname(GetPeer()) ? addr.IsLocalHost() : false;
}

void CRemoteConnect::WriteDoneAndQueueEmpty()
{
}

void CRemoteConnect::OnConnect() {
	// Apply the EC-tuned TCP keepalive timings now that the underlying
	// asio socket is fully connected on the async path (sync clients
	// got it inside CECMuleSocket::InternalConnect already; this is
	// the amulegui / amuleweb side where InternalConnect returns before
	// the connect actually completes).
	ApplyEcKeepalive();

	if (m_notifier) {
		wxASSERT(m_ec_state == EC_CONNECT_SENT);
		CECLoginPacket login_req(m_client, m_version, m_canZLIB, m_canUTF8numbers, m_canNotify, m_preferNoZlib);
		CECSocket::SendPacket(&login_req);

		m_ec_state = EC_REQ_SENT;
	} else {
		// do nothing, calling code will take from here
	}
}

void CRemoteConnect::OnLost() {
	if (m_notifier) {
		// Notify app of failure — amulegui's wxEvent handler flips the
		// UI to "disconnected" and stops trying to update.
		wxECSocketEvent event(wxEVT_EC_CONNECTION,false,_("Connection failure"));
		m_notifier->AddPendingEvent(event);
		return;
	}
	// Headless EC clients (amulecmd, amuleweb) construct CRemoteConnect
	// with NULL m_notifier. Continuing to run would mean serving stale
	// data in amuleweb (HTTP requests still return the template shell
	// without live amuled data, no error visible to the user) or
	// sitting at the amulecmd prompt with a dead socket. Failing fast
	// is the right semantic — supervisor (systemd unit / docker /
	// shell loop) is the recovery layer and decides whether to restart.
	fprintf(stderr, "%s\n",
		(const char *)unicode2char(_("External Connection lost — exiting.")));
	fflush(stderr);
	// _exit instead of exit() because the asio worker thread that
	// just fired this is mid-callback; racing static destructors
	// against it risks the kind of use-after-free #748 was about.
	// The OS reaps file descriptors / sockets on exit anyway.
	_exit(1);
}

const CECPacket *CRemoteConnect::OnPacketReceived(const CECPacket *packet, uint32 trueSize)
{
	CECPacket *next_packet = 0;
	m_req_count--;
	packet->DebugPrint(true, trueSize);
	switch(m_ec_state) {
		case EC_REQ_SENT:
			if (ProcessAuthPacket(packet)) {
				CECAuthPacket passwdPacket(m_connectionPassword);
				CECSocket::SendPacket(&passwdPacket);
				m_ec_state = EC_PASSWD_SENT;
			}
			break;
		case EC_PASSWD_SENT:
			ProcessAuthPacket(packet);
			break;
		case EC_OK:
			if ( !m_req_fifo.empty() ) {
				CECPacketHandlerBase *handler = m_req_fifo.front();
				m_req_fifo.pop_front();
				if ( handler ) {
					handler->HandlePacket(packet);
				}
			} else {
				printf("EC error - packet received, but request fifo is empty\n");
			}
			break;
		default:
			break;
	}

	// no reply by default
	return next_packet;
}

/*
 * Our requests are served by core in FCFS order. And core always replies. So, even
 * if we're not interested in reply, we preserve place in request fifo.
 */
void CRemoteConnect::SendRequest(CECPacketHandlerBase *handler, const CECPacket *request)
{
	m_req_count++;
	m_req_fifo.push_back(handler);
	CECSocket::SendPacket(request);
}

void CRemoteConnect::SendPacket(const CECPacket *request)
{
	SendRequest(0, request);
}

bool CRemoteConnect::ProcessAuthPacket(const CECPacket *reply) {
	bool result = false;

	if (!reply) {
		m_server_reply = _("EC connection failed. Empty reply.");
		CloseSocket();
	} else {
		if ((m_ec_state == EC_REQ_SENT) && (reply->GetOpCode() == EC_OP_AUTH_SALT)) {
				const CECTag *passwordSalt = reply->GetTagByName(EC_TAG_PASSWD_SALT);
				if ( NULL != passwordSalt) {
					wxString saltHash = MD5Sum(CFormat("%lX") % passwordSalt->GetInt()).GetHash();
					m_connectionPassword = MD5Sum(m_connectionPassword.Lower() + saltHash).GetHash();
					m_ec_state = EC_SALT_RECEIVED;
					return true;
				} else {
					m_server_reply = _("External Connection: Bad reply, handshake failed. Connection closed.");
					m_ec_state = EC_FAIL;
					CloseSocket();
				}
		} else if ((m_ec_state == EC_PASSWD_SENT) && (reply->GetOpCode() == EC_OP_AUTH_OK)) {
			m_ec_state = EC_OK;
			result = true;
			if (reply->GetTagByName(EC_TAG_SERVER_VERSION)) {
				m_server_reply = _("Succeeded! Connection established to aMule ") +
					reply->GetTagByName(EC_TAG_SERVER_VERSION)->GetStringData();
			} else {
				m_server_reply = _("Succeeded! Connection established.");
			}
			// Mirror server's negotiated capabilities into m_my_flags so
			// outgoing per-packet flags include them (auto-stripped by
			// `flags &= m_my_flags` in CECSocket::WritePacket otherwise).
			// EC_TAG_CAN_LARGE_TAG_COUNT only appears in AUTH_OK from
			// new daemons (#199); old daemons just don't echo the tag,
			// and the sentinel wire format stays disabled in both
			// directions for the duration of this connection.
			if (reply->GetTagByName(EC_TAG_CAN_LARGE_TAG_COUNT)) {
				m_my_flags |= EC_FLAG_LARGE_TAG_COUNT;
			}
			// Server confirms it speaks the partial-update protocol:
			// `Get_EC_Response_GetUpdate` may now omit unchanged files
			// and emit explicit `EC_TAG_FILE_REMOVED` markers, and our
			// INC_UPDATE handler must skip the bulk
			// "missing-from-response == deleted" loop. Old daemons
			// (#727 pre-fix or earlier) don't echo this tag and the
			// client stays on the legacy bulk-deletion path, which
			// the new server keeps compatible by emitting alive-marker
			// tags for unchanged files (#713).
			if (reply->GetTagByName(EC_TAG_CAN_PARTIAL_UPDATE)) {
				m_serverPartialUpdate = true;
			}
		}else {
			m_ec_state = EC_FAIL;
			const CECTag *reason = reply->GetTagByName(EC_TAG_STRING);
			if (reason != NULL) {
				m_server_reply = wxString(_("External Connection: Access denied because: ")) +
					wxGetTranslation(reason->GetStringData());
			} else {
				m_server_reply = _("External Connection: Handshake failed.");
			}
			CloseSocket();
		}
	}
	if ( m_notifier ) {
		wxECSocketEvent event(wxEVT_EC_CONNECTION, result, m_server_reply);
		m_notifier->AddPendingEvent(event);
	}
	return result;
}

/******************** EC API ***********************/

void CRemoteConnect::StartKad() {
	CECPacket req(EC_OP_KAD_START);
	SendPacket(&req);
}

void CRemoteConnect::StopKad() {
	CECPacket req(EC_OP_KAD_STOP);
	SendPacket(&req);
}

void CRemoteConnect::ConnectED2K(uint32 ip, uint16 port) {
	CECPacket req(EC_OP_SERVER_CONNECT);
	if (ip && port) {
		req.AddTag(CECTag(EC_TAG_SERVER, EC_IPv4_t(ip, port)));
	}
	SendPacket(&req);
}

void CRemoteConnect::DisconnectED2K() {
	CECPacket req(EC_OP_SERVER_DISCONNECT);
	SendPacket(&req);
}

void CRemoteConnect::RemoveServer(uint32 ip, uint16 port) {
	CECPacket req(EC_OP_SERVER_REMOVE);
	if (ip && port) {
		req.AddTag(CECTag(EC_TAG_SERVER, EC_IPv4_t(ip, port)));
	}
	SendPacket(&req);
}
// File_checked_for_headers
