// See the file "COPYING" in the main distribution directory for copyright.

#include "zeek/analyzer/protocol/gnutella/Gnutella.h"

#include "zeek/zeek-config.h"

#include <ctype.h>
#include <algorithm>

#include "zeek/Event.h"
#include "zeek/NetVar.h"
#include "zeek/analyzer/Manager.h"
#include "zeek/analyzer/protocol/gnutella/events.bif.h"
#include "zeek/analyzer/protocol/pia/PIA.h"

namespace zeek::analyzer::gnutella
	{

namespace detail
	{

GnutellaMsgState::GnutellaMsgState()
	{
	buffer = "";
	current_offset = 0;
	headers = "";
	msg_hops = 0;
	msg_len = 0;
	msg_pos = 0;
	msg_type = 0;
	msg_sent = 1;
	msg_ttl = 0;
	payload_left = 0;
	got_CR = 0;
	payload_len = 0;
	}

	} // namespace detail

Gnutella_Analyzer::Gnutella_Analyzer(Connection* conn)
	: analyzer::tcp::TCP_ApplicationAnalyzer("GNUTELLA", conn)
	{
	state = 0;
	new_state = 0;
	sent_establish = 0;

	ms = nullptr;

	orig_msg_state = new detail::GnutellaMsgState();
	resp_msg_state = new detail::GnutellaMsgState();
	}

Gnutella_Analyzer::~Gnutella_Analyzer()
	{
	delete orig_msg_state;
	delete resp_msg_state;
	}

void Gnutella_Analyzer::Done()
	{
	analyzer::tcp::TCP_ApplicationAnalyzer::Done();

	if ( ! sent_establish && (gnutella_establish || gnutella_not_establish) )
		{
		if ( Established() && gnutella_establish )
			EnqueueConnEvent(gnutella_establish, ConnVal());
		else if ( ! Established() && gnutella_not_establish )
			EnqueueConnEvent(gnutella_not_establish, ConnVal());
		}

	if ( gnutella_partial_binary_msg )
		{
		detail::GnutellaMsgState* p = orig_msg_state;

		for ( int i = 0; i < 2; ++i, p = resp_msg_state )
			{
			if ( ! p->msg_sent && p->msg_pos )
				EnqueueConnEvent(gnutella_partial_binary_msg, ConnVal(),
				                 make_intrusive<StringVal>(p->msg), val_mgr->Bool((i == 0)),
				                 val_mgr->Count(p->msg_pos));

			else if ( ! p->msg_sent && p->payload_left )
				SendEvents(p, (i == 0));
			}
		}
	}

bool Gnutella_Analyzer::NextLine(const u_char* data, int len)
	{
	if ( ! ms )
		return false;

	if ( Established() || ms->current_offset >= len )
		return false;

	for ( ; ms->current_offset < len; ++ms->current_offset )
		{
		if ( data[ms->current_offset] == '\r' )
			ms->got_CR = 1;

		else if ( data[ms->current_offset] == '\n' && ms->got_CR )
			{
			ms->got_CR = 0;
			++ms->current_offset;
			return true;
			}
		else
			ms->buffer += data[ms->current_offset];
		}

	return false;
	}

bool Gnutella_Analyzer::IsHTTP(std::string header)
	{
	if ( header.find(" HTTP/1.") == std::string::npos )
		return false;

	if ( gnutella_http_notify )
		EnqueueConnEvent(gnutella_http_notify, ConnVal());

	analyzer::Analyzer* a = analyzer_mgr->InstantiateAnalyzer("HTTP", Conn());

	if ( a && Parent()->AddChildAnalyzer(a) )
		{
		if ( Parent()->IsAnalyzer("TCP") )
			{
			// Replay buffered data.
			analyzer::pia::PIA* pia =
				static_cast<packet_analysis::IP::SessionAdapter*>(Parent())->GetPIA();
			if ( pia )
				static_cast<analyzer::pia::PIA_TCP*>(pia)->ReplayStreamBuffer(a);
			}

		Parent()->RemoveChildAnalyzer(this);
		}

	return true;
	}

bool Gnutella_Analyzer::GnutellaOK(std::string header)
	{
	if ( strncmp("GNUTELLA", header.data(), 8) )
		return false;

	int codepos = header.find(' ') + 1;
	if ( ! strncmp("200", header.data() + codepos, 3) )
		return true;

	return false;
	}

void Gnutella_Analyzer::DeliverLines(int len, const u_char* data, bool orig)
	{
	if ( ! ms )
		return;

	while ( NextLine(data, len) )
		{
		if ( ms->buffer.length() )
			{
			if ( ms->headers.length() == 0 )
				{
				if ( IsHTTP(ms->buffer) )
					return;
				if ( GnutellaOK(ms->buffer) )
					new_state |= orig ? ORIG_OK : RESP_OK;
				}

			ms->headers = ms->headers + "\r\n" + ms->buffer;
			ms->buffer = "";
			}
		else
			{
			if ( gnutella_text_msg )
				EnqueueConnEvent(gnutella_text_msg, ConnVal(), val_mgr->Bool(orig),
				                 make_intrusive<StringVal>(ms->headers.data()));

			ms->headers = "";
			state |= new_state;

			if ( Established() && gnutella_establish )
				{
				sent_establish = 1;

				EnqueueConnEvent(gnutella_establish, ConnVal());
				}
			}
		}
	}

void Gnutella_Analyzer::DissectMessage(char* msg)
	{
	if ( ! ms )
		return;

	ms->msg_type = msg[16];
	ms->msg_ttl = msg[17];
	ms->msg_hops = msg[18];

	memcpy(&ms->msg_len, &msg[19], 4);
	}

void Gnutella_Analyzer::SendEvents(detail::GnutellaMsgState* p, bool is_orig)
	{
	if ( p->msg_sent )
		return;

	if ( gnutella_binary_msg )
		EnqueueConnEvent(gnutella_binary_msg, ConnVal(), val_mgr->Bool(is_orig),
		                 val_mgr->Count(p->msg_type), val_mgr->Count(p->msg_ttl),
		                 val_mgr->Count(p->msg_hops), val_mgr->Count(p->msg_len),
		                 make_intrusive<StringVal>(p->payload), val_mgr->Count(p->payload_len),
		                 val_mgr->Bool((p->payload_len <
		                                std::min(p->msg_len, (unsigned int)GNUTELLA_MAX_PAYLOAD))),
		                 val_mgr->Bool((p->payload_left == 0)));
	}

void Gnutella_Analyzer::DeliverMessages(int len, const u_char* data, bool orig)
	{
	if ( ! ms )
		return;

	while ( ms->current_offset < len )
		{
		ms->msg_sent = 0;

		unsigned int bytes_left = len - ms->current_offset;
		unsigned int needed = 0;

		if ( ms->msg_pos )
			needed = GNUTELLA_MSG_SIZE - ms->msg_pos;

		if ( (! ms->msg_pos && ! ms->payload_left && (bytes_left >= GNUTELLA_MSG_SIZE)) ||
		     (ms->msg_pos && (bytes_left >= needed)) )
			{
			int sz = ms->msg_pos ? needed : GNUTELLA_MSG_SIZE;

			memcpy(&ms->msg[ms->msg_pos], &data[ms->current_offset], sz);

			ms->current_offset += sz;
			DissectMessage(ms->msg);
			ms->payload_left = ms->msg_len;
			ms->msg_pos = 0;
			if ( ms->msg_len == 0 )
				SendEvents(ms, orig);
			}

		else if ( (! ms->msg_pos && ! ms->payload_left && (bytes_left < GNUTELLA_MSG_SIZE)) ||
		          (ms->msg_pos && (bytes_left < needed)) )
			{
			memcpy(&ms->msg[ms->msg_pos], &data[ms->current_offset], bytes_left);
			ms->current_offset += bytes_left;
			ms->msg_pos += bytes_left;
			}

		else if ( ms->payload_left )
			{
			unsigned int space = ms->payload_len >= GNUTELLA_MAX_PAYLOAD
			                         ? 0
			                         : GNUTELLA_MAX_PAYLOAD - ms->payload_len;
			unsigned int sz = (bytes_left < space) ? bytes_left : space;

			if ( space )
				{
				memcpy(&ms->payload[ms->payload_len], &data[ms->current_offset], sz);
				ms->payload_len += sz;
				}

			if ( ms->payload_left > bytes_left )
				{
				ms->current_offset += bytes_left;
				ms->payload_left -= bytes_left;
				}
			else
				{
				ms->current_offset += ms->payload_left;
				ms->payload_left = 0;
				SendEvents(ms, orig);
				}
			}
		}
	}

void Gnutella_Analyzer::DeliverStream(int len, const u_char* data, bool orig)
	{
	analyzer::tcp::TCP_ApplicationAnalyzer::DeliverStream(len, data, orig);

	ms = orig ? orig_msg_state : resp_msg_state;
	ms->current_offset = 0;
	if ( ! Established() )
		{
		DeliverLines(len, data, orig);

		if ( Established() && ms->current_offset < len && gnutella_binary_msg )
			DeliverMessages(len, data, orig);
		}

	else if ( gnutella_binary_msg )
		DeliverMessages(len, data, orig);
	}

	} // namespace zeek::analyzer::gnutella
