/*-
 * SPDX-License-Identifier: BSD-2-Clause
 *
 * Copyright (C) 2008 Edwin Groothuis. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

#include <sys/param.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/time.h>

#include <netinet/in.h>
#include <arpa/tftp.h>

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>

#include "tftp-file.h"
#include "tftp-io.h"
#include "tftp-utils.h"
#include "tftp-options.h"
#include "tftp-transfer.h"

struct block_data {
	off_t offset;
	uint16_t block;
	int size;
};

/*
 * Send a file via the TFTP data session.
 */
int
tftp_send(int peer, uint16_t *block, struct tftp_stats *ts)
{
	struct tftphdr *rp;
	int size, n_data, n_ack, sendtry, acktry;
	u_int i, j;
	uint16_t oldblock, windowblock;
	char sendbuffer[MAXPKTSIZE];
	char recvbuffer[MAXPKTSIZE];
	struct block_data window[WINDOWSIZE_MAX];

	rp = (struct tftphdr *)recvbuffer;
	*block = 1;
	ts->amount = 0;
	windowblock = 0;
	acktry = 0;
	do {
read_block:
		if (debug & DEBUG_SIMPLE)
			tftp_log(LOG_DEBUG, "Sending block %d (window block %d)",
			    *block, windowblock);

		window[windowblock].offset = tell_file();
		window[windowblock].block = *block;
		size = read_file(sendbuffer, segsize);
		if (size < 0) {
			tftp_log(LOG_ERR, "read_file returned %d", size);
			send_error(peer, errno + 100);
			return -1;
		}
		window[windowblock].size = size;
		windowblock++;

		for (sendtry = 0; ; sendtry++) {
			n_data = send_data(peer, *block, sendbuffer, size);
			if (n_data == 0)
				break;

			if (sendtry == maxtimeouts) {
				tftp_log(LOG_ERR,
				    "Cannot send DATA packet #%d, "
				    "giving up", *block);
				return -1;
			}
			tftp_log(LOG_ERR,
			    "Cannot send DATA packet #%d, trying again",
			    *block);
		}

		/* Only check for ACK for last block in window. */
		if (windowblock == windowsize || size != segsize) {
			n_ack = receive_packet(peer, recvbuffer,
			    MAXPKTSIZE, NULL, timeoutpacket);
			if (n_ack < 0) {
				if (n_ack == RP_TIMEOUT) {
					if (acktry == maxtimeouts) {
						tftp_log(LOG_ERR,
						    "Timeout #%d send ACK %d "
						    "giving up", acktry, *block);
						return -1;
					}
					tftp_log(LOG_WARNING,
					    "Timeout #%d on ACK %d",
					    acktry, *block);

					acktry++;
					ts->retries++;
					if (seek_file(window[0].offset) != 0) {
						tftp_log(LOG_ERR,
						    "seek_file failed: %s",
						    strerror(errno));
						send_error(peer, errno + 100);
						return -1;
					}
					*block = window[0].block;
					windowblock = 0;
					goto read_block;
				}

				/* Either read failure or ERROR packet */
				if (debug & DEBUG_SIMPLE)
					tftp_log(LOG_ERR, "Aborting: %s",
					    rp_strerror(n_ack));
				return -1;
			}
			if (rp->th_opcode == ACK) {
				/*
				 * Look for the ACKed block in our open
				 * window.
				 */
				for (i = 0; i < windowblock; i++) {
					if (rp->th_block == window[i].block)
						break;
				}

				if (i == windowblock) {
					/* Did not recognize ACK. */
					if (debug & DEBUG_SIMPLE)
						tftp_log(LOG_DEBUG,
						    "ACK %d out of window",
						    rp->th_block);

					/* Re-synchronize with the other side */
					(void) synchnet(peer);

					/* Resend the current window. */
					ts->retries++;
					if (seek_file(window[0].offset) != 0) {
						tftp_log(LOG_ERR,
						    "seek_file failed: %s",
						    strerror(errno));
						send_error(peer, errno + 100);
						return -1;
					}
					*block = window[0].block;
					windowblock = 0;
					goto read_block;
				}

				/* ACKed at least some data. */
				acktry = 0;
				for (j = 0; j <= i; j++) {
					if (debug & DEBUG_SIMPLE)
						tftp_log(LOG_DEBUG,
						    "ACKed block %d",
						    window[j].block);
					ts->blocks++;
					ts->amount += window[j].size;
				}

				/*
				 * Partial ACK.  Rewind state to first
				 * un-ACKed block.
				 */
				if (i + 1 != windowblock) {
					if (debug & DEBUG_SIMPLE)
						tftp_log(LOG_DEBUG,
						    "Partial ACK");
					if (seek_file(window[i + 1].offset) !=
					    0) {
						tftp_log(LOG_ERR,
						    "seek_file failed: %s",
						    strerror(errno));
						send_error(peer, errno + 100);
						return -1;
					}
					*block = window[i + 1].block;
					windowblock = 0;
					ts->retries++;
					goto read_block;
				}

				windowblock = 0;
			}

		}
		oldblock = *block;
		(*block)++;
		if (oldblock > *block) {
			if (options[OPT_ROLLOVER].o_request == NULL) {
				/*
				 * "rollover" option not specified in
				 * tftp client.  Default to rolling block
				 * counter to 0.
				 */
				*block = 0;
			} else {
				*block = atoi(options[OPT_ROLLOVER].o_request);
			}

			ts->rollovers++;
		}
		gettimeofday(&(ts->tstop), NULL);
	} while (size == segsize);
	return 0;
}

/*
 * Receive a file via the TFTP data session.
 *
 * - It could be that the first block has already arrived while
 *   trying to figure out if we were receiving options or not. In
 *   that case it is passed to this function.
 */
int
tftp_receive(int peer, uint16_t *block, struct tftp_stats *ts,
    struct tftphdr *firstblock, size_t fb_size)
{
	struct tftphdr *rp;
	uint16_t oldblock, windowstart;
	int n_data, n_ack, writesize, i, retry, windowblock;
	char recvbuffer[MAXPKTSIZE];

	ts->amount = 0;
	windowblock = 0;

	if (firstblock != NULL) {
		writesize = write_file(firstblock->th_data, fb_size);
		ts->amount += writesize;
		ts->blocks++;
		windowblock++;
		if (windowsize == 1 || fb_size != segsize) {
			for (i = 0; ; i++) {
				n_ack = send_ack(peer, *block);
				if (n_ack > 0) {
					if (i == maxtimeouts) {
						tftp_log(LOG_ERR,
						    "Cannot send ACK packet #%d, "
						    "giving up", *block);
						return -1;
					}
					tftp_log(LOG_ERR,
					    "Cannot send ACK packet #%d, trying again",
					    *block);
					continue;
				}

				break;
			}
		}

		if (fb_size != segsize) {
			write_close();
			gettimeofday(&(ts->tstop), NULL);
			return 0;
		}
	}

	rp = (struct tftphdr *)recvbuffer;
	do {
		oldblock = *block;
		(*block)++;
		if (oldblock > *block) {
			if (options[OPT_ROLLOVER].o_request == NULL) {
				/*
				 * "rollover" option not specified in
				 * tftp client.  Default to rolling block
				 * counter to 0.
				 */
				*block = 0;
			} else {
				*block = atoi(options[OPT_ROLLOVER].o_request);
			}

			ts->rollovers++;
		}

		for (retry = 0; ; retry++) {
			if (debug & DEBUG_SIMPLE)
				tftp_log(LOG_DEBUG,
				    "Receiving DATA block %d (window block %d)",
				    *block, windowblock);

			n_data = receive_packet(peer, recvbuffer,
			    MAXPKTSIZE, NULL, timeoutpacket);
			if (n_data < 0) {
				if (retry == maxtimeouts) {
					tftp_log(LOG_ERR,
					    "Timeout #%d on DATA block %d, "
					    "giving up", retry, *block);
					return -1;
				}
				if (n_data == RP_TIMEOUT) {
					tftp_log(LOG_WARNING,
					    "Timeout #%d on DATA block %d",
					    retry, *block);
					send_ack(peer, oldblock);
					windowblock = 0;
					continue;
				}

				/* Either read failure or ERROR packet */
				if (debug & DEBUG_SIMPLE)
					tftp_log(LOG_DEBUG, "Aborting: %s",
					    rp_strerror(n_data));
				return -1;
			}
			if (rp->th_opcode == DATA) {
				ts->blocks++;

				if (rp->th_block == *block)
					break;

				/*
				 * Ignore duplicate blocks within the
				 * window.
				 *
				 * This does not handle duplicate
				 * blocks during a rollover as
				 * gracefully, but that should still
				 * recover eventually.
				 */
				if (*block > windowsize)
					windowstart = *block - windowsize;
				else
					windowstart = 0;
				if (rp->th_block > windowstart &&
				    rp->th_block < *block) {
					if (debug & DEBUG_SIMPLE)
						tftp_log(LOG_DEBUG,
					    "Ignoring duplicate DATA block %d",
						    rp->th_block);
					windowblock++;
					retry = 0;
					continue;
				}

				tftp_log(LOG_WARNING,
				    "Expected DATA block %d, got block %d",
				    *block, rp->th_block);

				/* Re-synchronize with the other side */
				(void) synchnet(peer);

				tftp_log(LOG_INFO, "Trying to sync");
				*block = oldblock;
				ts->retries++;
				goto send_ack;	/* rexmit */

			} else {
				tftp_log(LOG_WARNING,
				    "Expected DATA block, got %s block",
				    packettype(rp->th_opcode));
			}
		}

		if (n_data > 0) {
			writesize = write_file(rp->th_data, n_data);
			ts->amount += writesize;
			if (writesize <= 0) {
				tftp_log(LOG_ERR,
				    "write_file returned %d", writesize);
				if (writesize < 0)
					send_error(peer, errno + 100);
				else
					send_error(peer, ENOSPACE);
				return -1;
			}
		}
		if (n_data != segsize)
			write_close();
		windowblock++;

		/* Only send ACKs for the last block in the window. */
		if (windowblock < windowsize && n_data == segsize)
			continue;
send_ack:
		for (i = 0; ; i++) {
			n_ack = send_ack(peer, *block);
			if (n_ack > 0) {

				if (i == maxtimeouts) {
					tftp_log(LOG_ERR,
					    "Cannot send ACK packet #%d, "
					    "giving up", *block);
					return -1;
				}

				tftp_log(LOG_ERR,
				    "Cannot send ACK packet #%d, trying again",
				    *block);
				continue;
			}

			if (debug & DEBUG_SIMPLE)
				tftp_log(LOG_DEBUG, "Sent ACK for %d", *block);
			windowblock = 0;
			break;
		}
		gettimeofday(&(ts->tstop), NULL);
	} while (n_data == segsize);

	/* Don't do late packet management for the client implementation */
	if (acting_as_client)
		return 0;

	for (i = 0; ; i++) {
		n_data = receive_packet(peer, (char *)rp, pktsize,
		    NULL, -timeoutpacket);
		if (n_data <= 0)
			break;
		if (n_data > 0 &&
		    rp->th_opcode == DATA &&	/* and got a data block */
		    *block == rp->th_block)	/* then my last ack was lost */
			send_ack(peer, *block);	/* resend final ack */
	}

	return 0;
}
