# Copyright (c) 2021 by the Zeek Project. See LICENSE for details.

module TFTP;

export {
	redef enum Log::ID += { LOG };

	type Info: record {
		## Timestamp for when the request happened.
		ts:		time &log;
		## Unique ID for the connection.
		uid:		string &log;
		## The connection's 4-tuple of endpoint addresses/ports.
		id:		conn_id &log;
		## True  for write requests, False for read request.
		wrq:		bool &log;
		## File name of request.
		fname:		string &log;
		## Mode of request.
		mode:		string &log;
		## UID of data connection
		uid_data:	string &optional &log;
		## Number of bytes sent.
		size:		count &default=0 &log;
		## Highest block number sent.
		block_sent:	count &default=0 &log;
		## Highest block number ackknowledged.
		block_acked:	count &default=0 &log;
		## Any error code encountered.
		error_code:	count &optional &log;
		## Any error message encountered.
		error_msg:	string &optional &log;

		# Set to block number of final piece of data once received.
		final_block: count &optional;

		# Set to true once logged.
		done: bool &default=F;
	};

	## Event that can be handled to access the TFTP logging record.
	global log_tftp: event(rec: Info);
}

# Maps a partial data connection ID to the request's Info record.
global expected_data_conns: table[addr, port, addr] of Info;

redef record connection += {
	tftp: Info &optional;
};

event zeek_init() &priority=5
	{
	Log::create_stream(TFTP::LOG, [$columns = Info, $ev = log_tftp, $path="tftp"]);
	}

function log_pending(c: connection)
	{
	if ( ! c?$tftp || c$tftp$done )
		return;

	Log::write(TFTP::LOG, c$tftp);
	c$tftp$done = T;
	}

function init_request(c: connection, is_orig: bool, fname: string, mode: string, is_read: bool)
	{
	log_pending(c);

	local info: Info;
	info$ts  = network_time();
	info$uid = c$uid;
	info$id  = c$id;
	info$fname = fname;
	info$mode = mode;
	info$wrq = (! is_read);
	c$tftp = info;

	# The data will come in from a different source port.
        #
	# TODO: Starting with Zeek 5.2, we will be able to use
	# Analyzer::ANALYZER_SPICY_TFTP instead of get_tag(),
	Analyzer::schedule_analyzer(c$id$resp_h, c$id$orig_h, c$id$orig_p, Analyzer::get_tag("Spicy_TFTP"), 1min);
	expected_data_conns[c$id$resp_h, c$id$orig_p, c$id$orig_h] = info;
	}

event scheduled_analyzer_applied(c: connection, a: Analyzer::Tag) &priority=10
	{
	local id = c$id;
	if ( [c$id$orig_h, c$id$resp_p, c$id$resp_h] in expected_data_conns )
		{
		c$tftp = expected_data_conns[c$id$orig_h, c$id$resp_p, c$id$resp_h];
		c$tftp$uid_data = c$uid;
		add c$service["spicy_tftp_data"];
		}
	}

event tftp::read_request(c: connection, is_orig: bool, fname: string, mode: string)
	{
	init_request(c, is_orig, fname, mode, T);
	}

event tftp::write_request(c: connection, is_orig: bool, fname: string, mode: string)
	{
	init_request(c, is_orig, fname, mode, F);
	}

event tftp::data(c: connection, is_orig: bool, block_num: count, data: string)
	{
	if ( ! c?$tftp || c$tftp$done )
		return;

	local info = c$tftp;

	if ( block_num <= info$block_sent )
		# Duplicate (or previous gap; we don't track that)
		return;

	info$size += |data|;
	info$block_sent = block_num;

	if ( |data| < 512 )
		# Last block, per spec.
		info$final_block = block_num;
	}

event tftp::ack(c: connection, is_orig: bool, block_num: count)
	{
	if ( ! c?$tftp || c$tftp$done )
		return;

	local info = c$tftp;

	info$block_acked = block_num;

	if ( block_num <= info$block_acked )
		# Duplicate (or previous gap, we don't track that)
		return;

	info$block_acked = block_num;

	# If it's an ack for the last block, we're done.
	if ( info?$final_block && info$final_block == block_num )
		log_pending(c);
	}

event tftp::error(c: connection, is_orig: bool, code: count, msg: string)
	{
	if ( ! c?$tftp || c$tftp$done )
		return;

	local info = c$tftp;

	info$error_code = code;
	info$error_msg = msg;
	log_pending(c);
	}

event connection_state_remove(c: connection)
	{
	if ( ! c?$tftp || c$tftp$done )
		return;

	log_pending(c);
	}
