//
// RemoteSyslogListener.cpp
//
// Library: Net
// Package: Logging
// Module:  RemoteSyslogListener
//
// Copyright (c) 2007, Applied Informatics Software Engineering GmbH.
// and Contributors.
//
// SPDX-License-Identifier:	BSL-1.0
//


#include "Poco/Net/RemoteSyslogListener.h"
#include "Poco/Net/RemoteSyslogChannel.h"
#include "Poco/Net/DatagramSocket.h"
#include "Poco/Net/SocketAddress.h"
#include "Poco/Runnable.h"
#include "Poco/Notification.h"
#include "Poco/AutoPtr.h"
#include "Poco/NumberParser.h"
#include "Poco/NumberFormatter.h"
#include "Poco/DateTimeParser.h"
#include "Poco/Message.h"
#include "Poco/LoggingFactory.h"
#include "Poco/Buffer.h"
#include "Poco/Ascii.h"
#include <cstddef>


namespace Poco {
namespace Net {


//
// MessageNotification
//


class MessageNotification: public Poco::Notification
{
public:
	MessageNotification(const char* buffer, std::size_t length, const Poco::Net::SocketAddress& sourceAddress):
		_message(buffer, length),
		_sourceAddress(sourceAddress)
	{
	}

	MessageNotification(const std::string& message, const Poco::Net::SocketAddress& sourceAddress):
		_message(message),
		_sourceAddress(sourceAddress)
	{
	}

	~MessageNotification()
	{
	}

	const std::string& message() const
	{
		return _message;
	}

	const Poco::Net::SocketAddress& sourceAddress() const
	{
		return _sourceAddress;
	}

private:
	std::string _message;
	Poco::Net::SocketAddress _sourceAddress;
};


//
// RemoteUDPListener
//


class RemoteUDPListener: public Poco::Runnable
{
public:
	enum
	{
		WAITTIME_MILLISEC = 1000,
		BUFFER_SIZE = 65536
	};

	RemoteUDPListener(Poco::NotificationQueue& queue, Poco::UInt16 port, bool reusePort, int buffer);
	~RemoteUDPListener();

	void run();
	void safeStop();

private:
	Poco::NotificationQueue& _queue;
	DatagramSocket           _socket;
	std::atomic<bool>        _stopped;
};


RemoteUDPListener::RemoteUDPListener(Poco::NotificationQueue& queue, Poco::UInt16 port, bool reusePort, int buffer):
	_queue(queue),
	_socket(Poco::Net::SocketAddress(Poco::Net::IPAddress(), port), false, reusePort),
	_stopped(false)
{
	if (buffer > 0)
	{
		_socket.setReceiveBufferSize(buffer);
	}
}


RemoteUDPListener::~RemoteUDPListener()
{
}


void RemoteUDPListener::run()
{
	Poco::Buffer<char> buffer(BUFFER_SIZE);
	Poco::Timespan waitTime(WAITTIME_MILLISEC* 1000);
	while (!_stopped)
	{
		try
		{
			if (_socket.poll(waitTime, Socket::SELECT_READ))
			{
				Poco::Net::SocketAddress sourceAddress;
				int n = _socket.receiveFrom(buffer.begin(), BUFFER_SIZE, sourceAddress);
				if (n > 0)
				{
					_queue.enqueueNotification(new MessageNotification(buffer.begin(), n, sourceAddress));
				}
			}
		}
		catch (...)
		{
			// lazy exception catching
		}
	}
}


void RemoteUDPListener::safeStop()
{
	_stopped = true;
}


//
// SyslogParser
//


class SyslogParser: public Poco::Runnable
{
public:
	static const std::string NILVALUE;

	enum
	{
		WAITTIME_MILLISEC = 1000
	};

	SyslogParser(Poco::NotificationQueue& queue, RemoteSyslogListener* pListener);
	~SyslogParser();

	void parse(const std::string& line, Poco::Message& message);
	void run();
	void safeStop();

	static Poco::Message::Priority convert(RemoteSyslogChannel::Severity severity);

private:
	void parsePrio(const std::string& line, std::size_t& pos, RemoteSyslogChannel::Severity& severity, RemoteSyslogChannel::Facility& fac);
	void parseNew(const std::string& line, RemoteSyslogChannel::Severity severity, RemoteSyslogChannel::Facility fac, std::size_t& pos, Poco::Message& message);
	void parseBSD(const std::string& line, RemoteSyslogChannel::Severity severity, RemoteSyslogChannel::Facility fac, std::size_t& pos, Poco::Message& message);

	static std::string parseUntilSpace(const std::string& line, std::size_t& pos);
		/// Parses until it encounters the next space char, returns the string from pos, excluding space
		/// pos will point past the space char

	static std::string parseStructuredData(const std::string& line, std::size_t& pos);
		/// Parses the structured data field.

	static std::string parseStructuredDataToken(const std::string& line, std::size_t& pos);
	/// Parses a token from the structured data field.

private:
	Poco::NotificationQueue& _queue;
	std::atomic<bool>        _stopped;
	RemoteSyslogListener*    _pListener;
};


const std::string SyslogParser::NILVALUE("-");


SyslogParser::SyslogParser(Poco::NotificationQueue& queue, RemoteSyslogListener* pListener):
	_queue(queue),
	_stopped(false),
	_pListener(pListener)
{
	poco_check_ptr (_pListener);
}


SyslogParser::~SyslogParser()
{
}


void SyslogParser::run()
{
	while (!_stopped)
	{
		try
		{
			Poco::AutoPtr<Poco::Notification> pNf(_queue.waitDequeueNotification(WAITTIME_MILLISEC));
			if (pNf)
			{
				Poco::AutoPtr<MessageNotification> pMsgNf = pNf.cast<MessageNotification>();
				Poco::Message message;
				parse(pMsgNf->message(), message);
				message["addr"] =pMsgNf->sourceAddress().host().toString();
				_pListener->log(message);
			}
		}
		catch (Poco::Exception&)
		{
			// parsing exception, what should we do?
		}
		catch (...)
		{
		}
	}
}


void SyslogParser::safeStop()
{
	_stopped = true;
}


void SyslogParser::parse(const std::string& line, Poco::Message& message)
{
	// <int> -> int: lower 3 bits severity, upper bits: facility
	std::size_t pos = 0;
	RemoteSyslogChannel::Severity severity;
	RemoteSyslogChannel::Facility fac;
	parsePrio(line, pos, severity, fac);

	// the next field decide if we parse an old BSD message or a new syslog message
	// BSD: expects a month value in string form: Jan, Feb...
	// SYSLOG expects a version number: 1

	if (Poco::Ascii::isDigit(line[pos]))
	{
		parseNew(line, severity, fac, pos, message);
	}
	else
	{
		parseBSD(line, severity, fac, pos, message);
	}
	poco_assert (pos == line.size());
}


void SyslogParser::parsePrio(const std::string& line, std::size_t& pos, RemoteSyslogChannel::Severity& severity, RemoteSyslogChannel::Facility& fac)
{
	poco_assert (pos < line.size());
	poco_assert (line[pos] == '<');
	++pos;
	std::size_t start = pos;

	while (pos < line.size() && Poco::Ascii::isDigit(line[pos]))
		++pos;

	poco_assert (line[pos] == '>');
	poco_assert (pos - start > 0);
	std::string valStr = line.substr(start, pos - start);
	++pos; // skip the >

	int val = Poco::NumberParser::parse(valStr);
	poco_assert (val >= 0 && val <= (static_cast<int>(RemoteSyslogChannel::SYSLOG_LOCAL7) + static_cast<int>(RemoteSyslogChannel::SYSLOG_DEBUG)));

	Poco::UInt16 pri = static_cast<Poco::UInt16>(val);
	// now get the lowest 3 bits
	severity = static_cast<RemoteSyslogChannel::Severity>(pri & 0x0007u);
	fac = static_cast<RemoteSyslogChannel::Facility>(pri & 0xfff8u);
}


void SyslogParser::parseNew(const std::string& line, RemoteSyslogChannel::Severity severity, RemoteSyslogChannel::Facility fac, std::size_t& pos, Poco::Message& message)
{
	Poco::Message::Priority prio = convert(severity);
	// rest of the unparsed header is:
	// VERSION SP TIMESTAMP SP HOSTNAME SP APP-NAME SP PROCID SP MSGID
	std::string versionStr(parseUntilSpace(line, pos));
	std::string timeStr(parseUntilSpace(line, pos)); // can be the nilvalue!
	std::string hostName(parseUntilSpace(line, pos));
	std::string appName(parseUntilSpace(line, pos));
	std::string procId(parseUntilSpace(line, pos));
	std::string msgId(parseUntilSpace(line, pos));
	std::string sd(parseStructuredData(line, pos));
	std::string messageText(line.substr(pos));
	pos = line.size();
	Poco::DateTime date;
	int tzd = 0;
	bool hasDate = Poco::DateTimeParser::tryParse(RemoteSyslogChannel::SYSLOG_TIMEFORMAT, timeStr, date, tzd);
	Poco::Message logEntry(msgId, messageText, prio);
	logEntry[RemoteSyslogListener::LOG_PROP_FACILITY] = RemoteSyslogChannel::facilityToString(fac);
	logEntry[RemoteSyslogListener::LOG_PROP_HOST] = hostName;
	logEntry[RemoteSyslogListener::LOG_PROP_APP] = appName;
	logEntry[RemoteSyslogListener::LOG_PROP_STRUCTURED_DATA] = sd;

	if (hasDate)
		logEntry.setTime(date.timestamp());
	int lval(0);
	Poco::NumberParser::tryParse(procId, lval);
	logEntry.setPid(lval);
	message.swap(logEntry);
}


void SyslogParser::parseBSD(const std::string& line, RemoteSyslogChannel::Severity severity, RemoteSyslogChannel::Facility fac, std::size_t& pos, Poco::Message& message)
{
	Poco::Message::Priority prio = convert(severity);
	// rest of the unparsed header is:
	// "%b %f %H:%M:%S" SP hostname|ipaddress
	// detect three spaces
	int spaceCnt = 0;
	std::size_t start = pos;
	while (spaceCnt < 3 && pos < line.size())
	{
		if (line[pos] == ' ')
		{
			spaceCnt++;
			if (spaceCnt == 1)
			{
				// size must be 3 chars for month
				if (pos - start != 3)
				{
					// probably a shortened time value, or the hostname
					// assume hostName
					Poco::Message logEntry(line.substr(start, pos-start), line.substr(pos+1), prio);
					message.swap(logEntry);
					return;
				}
			}
			else if (spaceCnt == 2)
			{
				// a day value!
				if (!(Poco::Ascii::isDigit(line[pos-1]) && (Poco::Ascii::isDigit(line[pos-2]) || Poco::Ascii::isSpace(line[pos-2]))))
				{
					// assume the next field is a hostname
					spaceCnt = 3;
				}
			}
			if (pos + 1 < line.size() && line[pos+1] == ' ')
			{
				// we have two spaces when the day value is smaller than 10!
				++pos; // skip one
			}
		}
		++pos;
	}
	std::string timeStr(line.substr(start, pos-start-1));
	int tzd(0);
	Poco::DateTime date;
	int year = date.year(); // year is not included, use the current one
	bool hasDate = Poco::DateTimeParser::tryParse(RemoteSyslogChannel::BSD_TIMEFORMAT, timeStr, date, tzd);
	if (hasDate)
	{
		int m = date.month();
		int d = date.day();
		int h = date.hour();
		int min = date.minute();
		int sec = date.second();
		date = Poco::DateTime(year, m, d, h, min, sec);
	}
	// next entry is host SP
	std::string hostName(parseUntilSpace(line, pos));

	// TAG: at most 32 alphanumeric chars, ANY non alphannumeric indicates start of message content
	// ignore: treat everything as content
	std::string messageText(line.substr(pos));
	pos = line.size();
	Poco::Message logEntry(hostName, messageText, prio);
	logEntry.setTime(date.timestamp());
	logEntry[RemoteSyslogListener::LOG_PROP_FACILITY] = RemoteSyslogChannel::facilityToString(fac);
	message.swap(logEntry);
}


std::string SyslogParser::parseUntilSpace(const std::string& line, std::size_t& pos)
{
	std::size_t start = pos;
	while (pos < line.size() && !Poco::Ascii::isSpace(line[pos]))
		++pos;
	// skip space
	++pos;
	return line.substr(start, pos-start-1);
}


std::string SyslogParser::parseStructuredData(const std::string& line, std::size_t& pos)
{
	std::string sd;
	if (pos < line.size())
	{
		if (line[pos] == '-')
		{
			++pos;
		}
		else if (line[pos] == '[')
		{
			std::string tok = parseStructuredDataToken(line, pos);
			while (tok == "[")
			{
				sd += tok;
				tok = parseStructuredDataToken(line, pos);
				while (tok != "]" && !tok.empty())
				{
					sd += tok;
					tok = parseStructuredDataToken(line, pos);
				}
				sd += tok;
				if (pos < line.size() && line[pos] == '[') tok = parseStructuredDataToken(line, pos);
			}
		}
		if (pos < line.size() && Poco::Ascii::isSpace(line[pos])) ++pos;
	}
	return sd;
}


std::string SyslogParser::parseStructuredDataToken(const std::string& line, std::size_t& pos)
{
	std::string tok;
	if (pos < line.size())
	{
		if (Poco::Ascii::isSpace(line[pos]) || line[pos] == '=' || line[pos] == '[' || line[pos] == ']')
		{
			tok += line[pos++];
		}
		else if (line[pos] == '"')
		{
			tok += line[pos++];
			while (pos < line.size() && line[pos] != '"')
			{
				tok += line[pos++];
			}
			tok += '"';
			if (pos < line.size()) pos++;
		}
		else
		{
			while (pos < line.size() && !Poco::Ascii::isSpace(line[pos]) && line[pos] != '=')
			{
				tok += line[pos++];
			}
		}
	}
	return tok;
}

Poco::Message::Priority SyslogParser::convert(RemoteSyslogChannel::Severity severity)
{
	switch (severity)
	{
	case RemoteSyslogChannel::SYSLOG_EMERGENCY:
		return Poco::Message::PRIO_FATAL;
	case RemoteSyslogChannel::SYSLOG_ALERT:
		return Poco::Message::PRIO_FATAL;
	case RemoteSyslogChannel::SYSLOG_CRITICAL:
		return Poco::Message::PRIO_CRITICAL;
	case RemoteSyslogChannel::SYSLOG_ERROR:
		return Poco::Message::PRIO_ERROR;
	case RemoteSyslogChannel::SYSLOG_WARNING:
		return Poco::Message::PRIO_WARNING;
	case RemoteSyslogChannel::SYSLOG_NOTICE:
		return Poco::Message::PRIO_NOTICE;
	case RemoteSyslogChannel::SYSLOG_INFORMATIONAL:
		return Poco::Message::PRIO_INFORMATION;
	case RemoteSyslogChannel::SYSLOG_DEBUG:
		return Poco::Message::PRIO_DEBUG;
	}
	throw Poco::LogicException("Illegal severity value in message");
}


//
// RemoteSyslogListener
//


const std::string RemoteSyslogListener::PROP_PORT("port");
const std::string RemoteSyslogListener::PROP_REUSE_PORT("reusePort");
const std::string RemoteSyslogListener::PROP_THREADS("threads");
const std::string RemoteSyslogListener::PROP_BUFFER("buffer");

const std::string RemoteSyslogListener::LOG_PROP_FACILITY("facility");
const std::string RemoteSyslogListener::LOG_PROP_APP("app");
const std::string RemoteSyslogListener::LOG_PROP_HOST("host");
const std::string RemoteSyslogListener::LOG_PROP_STRUCTURED_DATA("structured-data");


RemoteSyslogListener::RemoteSyslogListener():
	_pListener(nullptr),
	_pParser(nullptr),
	_port(RemoteSyslogChannel::SYSLOG_PORT),
	_reusePort(false),
	_threads(1),
	_buffer(0)
{
}


RemoteSyslogListener::RemoteSyslogListener(Poco::UInt16 port):
	_pListener(nullptr),
	_pParser(nullptr),
	_port(port),
	_reusePort(false),
	_threads(1),
	_buffer(0)
{
}


RemoteSyslogListener::RemoteSyslogListener(Poco::UInt16 port, int threads):
	_pListener(nullptr),
	_pParser(nullptr),
	_port(port),
	_reusePort(false),
	_threads(threads),
	_buffer(0)
{
}


RemoteSyslogListener::RemoteSyslogListener(Poco::UInt16 port, bool reusePort, int threads):
	_pListener(nullptr),
	_pParser(nullptr),
	_port(port),
	_reusePort(reusePort),
	_threads(threads),
	_buffer(0)
{
}


RemoteSyslogListener::~RemoteSyslogListener()
{
}


void RemoteSyslogListener::processMessage(const std::string& messageText)
{
	Poco::Message message;
	_pParser->parse(messageText, message);
	log(message);
}


void RemoteSyslogListener::enqueueMessage(const std::string& messageText, const Poco::Net::SocketAddress& senderAddress)
{
	_queue.enqueueNotification(new MessageNotification(messageText, senderAddress));
}


void RemoteSyslogListener::setProperty(const std::string& name, const std::string& value)
{
	if (name == PROP_PORT)
	{
		int val = Poco::NumberParser::parse(value);
		if (val >= 0 && val < 65536)
			_port = static_cast<Poco::UInt16>(val);
		else
			throw Poco::InvalidArgumentException("Not a valid port number", value);
	}
	else if (name == PROP_REUSE_PORT)
	{
		_reusePort = Poco::NumberParser::parseBool(value);
	}
	else if (name == PROP_THREADS)
	{
		int val = Poco::NumberParser::parse(value);
		if (val > 0 && val < 16)
			_threads = val;
		else
			throw Poco::InvalidArgumentException("Invalid number of threads", value);
	}
	else if (name == PROP_BUFFER)
	{
		_buffer = Poco::NumberParser::parse(value);
	}
	else
	{
		SplitterChannel::setProperty(name, value);
	}
}


std::string RemoteSyslogListener::getProperty(const std::string& name) const
{
	if (name == PROP_PORT)
		return Poco::NumberFormatter::format(_port);
	else if (name == PROP_REUSE_PORT)
		return Poco::NumberFormatter::format(_reusePort);
	else if (name == PROP_THREADS)
		return Poco::NumberFormatter::format(_threads);
	else if (name == PROP_BUFFER)
		return Poco::NumberFormatter::format(_buffer);
	else
		return SplitterChannel::getProperty(name);
}


void RemoteSyslogListener::open()
{
	SplitterChannel::open();
	_pParser = new SyslogParser(_queue, this);
	if (_port > 0)
	{
		_pListener = new RemoteUDPListener(_queue, _port, _reusePort, _buffer);
	}
	for (int i = 0; i < _threads; i++)
	{
		_threadPool.start(*_pParser);
	}
	if (_pListener)
	{
		_threadPool.start(*_pListener);
	}
}


void RemoteSyslogListener::close()
{
	if (_pListener)
	{
		_pListener->safeStop();
	}
	if (_pParser)
	{
		_pParser->safeStop();
	}
	_queue.clear();
	_threadPool.joinAll();
	delete _pListener;
	delete _pParser;
	_pListener = nullptr;
	_pParser = nullptr;
	SplitterChannel::close();
}


void RemoteSyslogListener::registerChannel()
{
	Poco::LoggingFactory::defaultFactory().registerChannelClass("RemoteSyslogListener", new Poco::Instantiator<RemoteSyslogListener, Poco::Channel>);
}


} } // namespace Poco::Net
