/* ------------------------------------------------------------------------- */
/* ap-tftp.c                                                                 */
/*                                                                           */
/* Version: 1.1                                                              */
/*                                                                           */
/* Copyright (C) 2004-2005 Jan Rafaj <jr-aputils at cedric dot unob dot cz>  */
/*                                                                           */
/* A simple tftp client for upgrading ATMEL AT76C510 WiSOC-based APs.        */
/* Supports ATMEL+INTERSIL boards (1.4x.y firmware) and ATMEL+RFMD boards    */
/* (0.x.y.z firmware).                                                       */
/* Modelled around TELLUS (GEMTEK/ATMEL OEM) and SMARTBRIDGES TFTP clients   */
/* functionality.                                                            */
/* Tested with TELLUS, D-Link and SmartBridges hardware.                     */
/* This program is part of AP-UTILS project (http://ap-utils.polesye.net)    */
/*                                                                           */
/* Loosely based on a simple tftp client for busybox.                        */
/* Tries to follow RFC1350.                                                  */
/* Only "octet" mode and "put" method supported.                             */
/*                                                                           */
/* Not implemented:                                                          */
/* - uploading of OEM (default) settings                                     */
/* - uploading of PATCH code                                                 */
/*                                                                           */
/* Code based on:                                                            */
/*                                                                           */
/* bb tftp: Copyright (C) 2001 Magnus Damm <damm@opensource.se>              */
/*                                                                           */
/* atftp:   Copyright (C) 2000 Jean-Pierre Lefebvre <helix@step.polymtl.ca>  */
/*                             and Remi Lefebvre <remi@debian.org>           */
/*                                                                           */
/* utftp:   Copyright (C) 1999 Uwe Ohse <uwe@ohse.de>                        */
/*                                                                           */
/* 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA   */
/*                                                                           */
/* ------------------------------------------------------------------------- */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/stat.h>
#include <netdb.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <fcntl.h>
#include "ap-utils.h"

#ifdef HAVE_GETTEXT
/* GNU gettext stuff*/
#include <locale.h>
#include <libgnuintl.h>
#define _(String) gettext (String)
#else
#define _(String) (String)
#endif

#define TFTP_BLOCKSIZE_DEFAULT 512 /* according to RFC 1350, do not change */
#define TFTP_TIMEOUT 2             /* # seconds waiting for single response */
#define TFTP_RETRIES 5             /* # retries with TFTP_TIMEOUT */
#undef FEATURE_TFTP_DEBUG

/* opcodes we support */
#define TFTP_WRQ   2
#define TFTP_DATA  3
#define TFTP_ACK   4
#define TFTP_ERROR 5

#ifdef FEATURE_TFTP_DEBUG
#define YES _("yes")
#define NO _("no")
#endif
#define ERR_READFW _("Error while read()ing firmware file")


/* This is necessary becouse of linkage to libap.a */
short ap_type = ATMEL410;
char *community = NULL;
struct in_addr ap_ip;

int tftp(int ip, const char *remotefile, int localfd, const char *wopt,
	 int woptlen, int maxdlen)
{
/* known errors server may respond with */
char *tftp_error_msg[8] = {
    _("Undefined error"),
    _("File not found"),
    _("Access violation"),
    _("Disk full or allocation error"),
    _("Illegal TFTP operation"),
    _("Unknown transfer ID"),
    _("File already exists"),
    _("No such user")
};
    const int port = 69;
    struct sockaddr_in to;
    struct sockaddr_in from;
    struct timeval tv;
    fd_set rfds;
    unsigned short tmp;
    int fromlen;
    int opcode = TFTP_WRQ;
    int tftp_bufsize = TFTP_BLOCKSIZE_DEFAULT;
    int timeout;
    int block_nr = 1;
    int finished = 0;
    int socketfd;
    int len;
    int bc = lseek(localfd, 0, SEEK_CUR);
    char *nl = "";
    char *cp;
    char *buf = malloc(tftp_bufsize);
    char buf2[64];

    printf("Trying to upload firmware to the AP...\n");

    if ((socketfd = socket(PF_INET, SOCK_DGRAM, 0)) < 0) {
	perror("socket()");
	return EXIT_FAILURE;
    }

    len = sizeof(to);
    memset(&to, 0, len);
    bind(socketfd, (struct sockaddr *)&to, len);

    to.sin_family = AF_INET;
    to.sin_port = htons(port);
    to.sin_addr.s_addr = ip;

    while (1) {
	timeout = TFTP_RETRIES;
	cp = buf;

	/* first create the opcode part */

	*((unsigned short *) cp) = htons(opcode);
	cp += 2;

	if (opcode == TFTP_WRQ) {
	    /* see if the filename fits into buf */
	    len = strlen(remotefile) + 1;
	    if ((cp + len + 1 + 6 + woptlen) >= &buf[tftp_bufsize - 1]) {
		printf(_("Remote-filename too long.\n"));
		break;
	    }

	    strncpy(cp, remotefile, len);
	    cp += len;

	    // ATMEL tftp client specific
	    *cp = 1;
	    cp++;

	    memcpy(cp, "octet", 6);
	    cp += 6;

	    // ATMEL tftp client specific ("upload auth. code")
	    memcpy(cp, wopt, woptlen);
	    cp += woptlen;
	}

	if (opcode == TFTP_DATA) {
	    *((unsigned short *) cp) = htons(block_nr);
	    cp += 2;

	    len = read(localfd, cp, tftp_bufsize);

	    if (len < 0) {
		sprintf(buf2, _("%sError in read()"), nl);
		perror(buf2);
		break;
	    }

	    block_nr++;

	    /*
	     * assure we wont upload more than maxdlen bytes
	     * even if total file length > maxdlen
	     */
	    if (bc + len > maxdlen) {
		len = maxdlen - bc;
		cp[len] = '\0';
	    }
	    bc += len;

	    if (len != tftp_bufsize)
		finished++;

	    cp += len;
	}

	/* send packet */

	do {
	    len = cp - buf;

#ifdef FEATURE_TFTP_DEBUG
	    printf(_("sending %u bytes\n"), len);
	    for (cp = buf; cp < &buf[len]; cp++)
		printf("%02x ", *cp & 0xFF);

	    printf("\n");
#endif
	    if (sendto(socketfd, buf, len, 0,
		(struct sockaddr *) &to, sizeof(to)) < 0)
	    {
		sprintf(buf2, _("%sError in sendto()"), nl);
		perror(buf2);
		len = -1;
		break;
	    }

	    /* receive packet */

	    memset(&from, 0, sizeof(from));
	    fromlen = sizeof(from);

	    tv.tv_sec = TFTP_TIMEOUT;
	    tv.tv_usec = 0;

	    FD_ZERO(&rfds);
	    FD_SET(socketfd, &rfds);

	    switch (select(FD_SETSIZE, &rfds, NULL, NULL, &tv)) {
		case 1:
		    len = recvfrom(socketfd, buf, tftp_bufsize, 0,
			(struct sockaddr *) &from, &fromlen);

		    if (len < 0) {
			sprintf(buf2, _("%sError in recvfrom()"), nl);
			perror(buf2);
			break;
		    }

		    /*
		    if (to.sin_port == htons(port))
			to.sin_port = from.sin_port;
		    */
		    if (to.sin_port == from.sin_port) {
#ifndef FEATURE_TFTP_DEBUG
			if (opcode == TFTP_DATA) {
			    sprintf(buf2, _("\rProgress: uploaded %3i%%."),
				bc * 100 / maxdlen);
			    write(fileno(stdout), buf2, strlen(buf2));
			    nl = "\n";
			}
#endif
			timeout = 0;
			break;
		    }

		    /* assume invalid packet */
		    printf(_("%sMalformed packet received. Aborting.\n"), nl);
		    len = -1;
		    break;

		case 0:
		    timeout--;
		    if (timeout == 0)
			len = -1;

		    printf(_("%sTimed out waiting for response from server "
			"(%i/%i).\n"),nl, TFTP_RETRIES - timeout, TFTP_RETRIES);
		    nl = "";
		    break;

		default:
		    len = -1;
		    sprintf(buf2, _("%sError in select()"), nl);
		    perror(buf2);
	    }
	} while (timeout && (len >= 0));

	if (len < 0)
	    break;

	/* process received packet */

	opcode = ntohs(*((unsigned short *) buf));
	tmp = ntohs(*((unsigned short *) &buf[2]));

#ifdef FEATURE_TFTP_DEBUG
	printf(_("Received %d bytes: %04x %04x\n"), len, opcode, tmp);
#endif

	if (opcode == TFTP_ERROR) {
	    sprintf(buf, "code %i", tmp);
	    if (tmp < (sizeof(tftp_error_msg) / sizeof(char *))) {
		strcat(buf, " (");
		strcat(buf, tftp_error_msg[tmp]);
		strcat(buf, ")");
	    }

	    printf(_("%sError: server responded with %s. Aborting.\n"),nl,buf);
	    break;
	}

	if (opcode == TFTP_ACK) {
	    if (tmp == (block_nr - 1)) {
		if (finished) {
		    printf(_("%sFlash programming in progress...\n"), nl);
		    sleep(5);
		    printf(_("Finished successfully.\n"));
		    break;
		}

		opcode = TFTP_DATA;
		continue;
	    }
	}
    }

    close(socketfd);
    free(buf);

    return finished ? EXIT_SUCCESS : EXIT_FAILURE;
}

void usage(char **argv)
{
    printf (_("PLEASE BE _ABSOLUTELY_ SURE TO READ MANPAGE PRIOR USE!!!\n"));
    printf (_(
	"\nUsage: %s -i IP -f firmware_file.rom [-c community] [-h]\n"),
	argv[0]);
}

int main(int argc, char **argv)
{
    struct stat sb;
    char *localfile = NULL;
    char *remotefile = NULL;
    char *cp;
    int fd;
    int result = 0;
    int i;
    static char wopt[256];
    int woptlen;
    int maxdlen;

#ifdef HAVE_GETTEXT
    /* locale support init */
    setlocale(LC_ALL, "");
    bindtextdomain("ap-utils", LOCALEDIR);
    textdomain("ap-utils");
#endif

    printf (_("TFTP client for upgrading firmware in ATMEL AT76C510 "
	    "WiSOC-based APs.\n"));
    printf (_("(C) 2004-2005 Jan Rafaj "
	    "<jr-aputils at cedric dot unob dot cz>\n"));

    if (argc == 1) {
	usage(argv);
	return EXIT_SUCCESS;
    }

    ap_ip.s_addr = 0;

    while ((i = getopt(argc, argv, "i:f:c:h")) != -1) {
	switch (i) {
	    case 'i':
		/* host = gethostbyname(optarg); */
		for (cp = optarg, i = 0; *cp && (cp = index(cp, '.')); cp++, i++);
		if (i < 3 || !(inet_aton(optarg, &ap_ip))) {
		    printf(_("Error: invalid IP address format given.\n"));
		    result = EXIT_FAILURE;
		    goto quit;
		}
		break;
	    case 'f': 
		localfile = strdup(optarg);
		break;
	    case 'c':
		community = strdup(optarg);
		break;
	    case 'h':
		usage(argv);
		result = EXIT_SUCCESS;
		goto quit;
	    default:
		result = EXIT_FAILURE;
		goto quit;
	}
    }

    /*
     * either any of mandatory options (IP, firmware path) not specified,
     * or some extra non-option arguments specified
     */
    if (ap_ip.s_addr == 0 || !localfile || optind != argc) {
	printf(_("Error: invalid argument combination.\n"));
	result = EXIT_FAILURE;
	goto quit;
    }

    fd = open(localfile, O_RDONLY, 0644);
    if (fd < 0) {
	perror(_("Error while open()ing firmware file"));
	result = EXIT_FAILURE;
	goto quit;
    }

    fstat(fd, &sb);
    if (sb.st_size < 94448 || sb.st_size > 524288) {
	printf(_("Error: invalid firmware file given.\n"));
	result = EXIT_FAILURE;
	goto quit;
    }

    while ((i = read(fd, wopt, sizeof(wopt))) != 0) {
	if (i < 0) {
	    perror(ERR_READFW);
	    result = EXIT_FAILURE;
	    goto quit;
	}

	if (i == sizeof(wopt))
	    lseek(fd, -13, SEEK_CUR);

	while (i--) {
	    if (!(memcmp(&wopt[i], "ATMEL", 5)))
		result |= 1;

	    if (!(memcmp(&wopt[i], "802.11 AP", 9)))
		result |= 2;

	    if (!(memcmp(&wopt[i], "atbrfirm.bin", 12))) {
		result |= 16;
		remotefile = "atbrfirm.bin";
	    }

	    if (!(memcmp(&wopt[i], "atsingle.bin", 12))) {
		result |= 32;
		remotefile = "atsingle.bin";
	    }

	    if (result > 18)
		break;
	}

	if (result > 18)
	    break;

	memset(wopt, 0, sizeof(wopt));
    }
#if FEATURE_TFTP_DEBUG
    printf(_("Firmware file contains:\n"
	    "- string \"ATMEL\": %s\n"
	    "- string \"802.11 AP\": %s\n"
	    "- string \"atbrfirm.bin\": %s\n"
	    "- string \"atsingle.bin\": %s\n"),
	    result & 1 ? YES : NO, result & 2 ? YES : NO,
	    result & 16 ? YES : NO, result & 32 ? YES : NO);
#endif
    if ((result & (1|2)) != (1|2) || (result & (16|32)) == (16|32) ||
	(result & (16|32)) == 0)
    {
	printf(_("Error: invalid firmware file given.\n"));
	result = EXIT_FAILURE;
	goto quit;
    }

    lseek(fd, 0, SEEK_SET);

    cp = strrchr(localfile, '/');
    if (cp)
	cp++;
    else
	cp = localfile;

    printf(_("Using:\n"
	"- server: %s\n"
	"- firmware file: \"%s\"\n"
	"- name used for upload: \"%s\"\n"),
	inet_ntoa(ap_ip), cp, remotefile);

    if (result & 16) {
	/* Firmware series 1.4x.y - atbrfirm.bin */

	/*
	 * For most devices using 1.4x.y firmware:
	 * - at most 94448 bytes needs to be written (file content above
	 *   this offset is simply cut off)
	 * - checksum is only performed on the uploaded (first 94448)
	 *   bytes of the firmware file
	 * - auth. code has 8 bytes in length (see below for its structure)
	 * SmartBridges, however, differs in this:
	 * - the TFTP firmware upgrade utility first checks whether the OUI
	 *   portion of its MAC address is 00301A (SMARTBRIDGES PTE. LTD.),
	 *   followed by 09 (apparently AT76C510-based SmartBridges devices),
	 *   and will continue only upon match. This is to prevent performing
	 *   this SmartBridges-style upgrade to non-SmartBridges devices, that
	 *   could otherwise end up with damaged flash content. Note that
	 *   this is still somewhat clumsy, since there's a remote possibility
	 *   that someone redefined his/her ATMEL AP MAC address with
	 *   SmartBridges OUI and added 09, so this check would not prevent
	 *   damage in this case (ouch!). There's however, a better check
	 *   we could come up up with: lets borrow a part of the code from
	 *   get_mib_details() to find out whether the target device appears
	 *   to be running firmware with ATMEL410 SBRIDGES MIB, and proceed
	 *   with 'SmartBridges' upgrade method only, if it does.
	 * - entire file is being written; there's no 'first 94448 bytes only'
	 *   limitation
	 * - 8-byte auth. code (see below) is followed by:
	 *   - (char):   length of the following "password" string
	 *   - (string): "password" string - the firmware will perform
	 *               actual upgrade only if this "password" matches with
	 *               any of the three communities defined in the AP
	 *               (sysadmin's note: finally something reasonable)
	 */

	unsigned short sum = 0;
	unsigned short *wp;
	int readen = 0;

	if (community) {
	    /*
	     * OK, looks like we want to upgrade SmartBridges hardware => no
	     * firmware file cutoff, and lets check whether the target device
	     * appears to have SmartBridges firmware. If not, bail out.
	     */
/*
	    char operEthernetAddress[12] = {
		0x2B, 0x06, 0x01, 0x04, 0x01, 0x83, 0x1A,
		0x01, 0x01, 0x02, 0x03, 0x00
	    };
*/
	    char AuthRadiusIP[] = {
		0x2B, 0x06, 0x01, 0x04, 0x01, 0x83, 0x1A,
		0x01, 0x02, 0x06, 0x03, 0x00
	    };
	    varbind varbinds[1];

	    maxdlen = sb.st_size;

/*
	    varbinds[0].oid = operEthernetAddress;
	    varbinds[0].len_oid = sizeof(operEthernetAddress);
*/
	    varbinds[0].oid = AuthRadiusIP;
	    varbinds[0].len_oid = sizeof(AuthRadiusIP);
	    varbinds[0].value = NULL;
	    varbinds[0].len_val = 0;
	    varbinds[0].type = NULL_VALUE;
	    if (snmp(varbinds, 1, GET) > 0) {
/*
		i = varbinds[0].value[0] << 24 |
		    varbinds[0].value[1] << 16 |
		    varbinds[0].value[2] << 8  |
		    varbinds[0].value[3];
		if (i != 0x00301A09) {
*/
		if (varbinds[0].len_val != 4) {
		    printf(_("Error: device with the given IP does not seem "
			     "to run SmartBridges firmware.\n"));
		    result = EXIT_FAILURE;
		    goto quit;
		}
	    } else {
		printf(_("Error: SNMP authorization error or device unavailable"
		    ".\n"));
		result = EXIT_FAILURE;
		goto quit;
	    }
	} else {
	    /* non-SB hardware */
	    maxdlen = 94448;
	}

	/* compute checksum */
	while ((i = read(fd, wopt, sizeof(wopt))) != 0) {
	    if (i < 0) {
		perror(ERR_READFW);
		result = EXIT_FAILURE;
		goto quit;
	    }

	    /* assure we do not checksum more than maxdlen bytes */
	    if ((readen + i) > maxdlen)
		i = maxdlen - readen;

	    readen += i;

	    /* perform iterative checksumming */
	    i /= 2;
	    wp = (unsigned short *)wopt;
	    while (i--) {
		if (sum + *wp > 0xFFFF)
		    sum++;

		sum += *(wp++);
	    };

	    /* prevent eventual fread() if file length > maxdlen */
	    if (readen == maxdlen)
		break;
	}
	sum ^= 0xFFFF;
	lseek (fd, 0, SEEK_SET);

	/*
	 * wopt[0] - wopt[1] are fw length in unsigned short form
	 * wopt[2] - wopt[3] are fw checksum in unsigned short form
	 * wopt[4] - wopt[7] are fw length in unsigned int form
	 */
	wopt[0] = maxdlen;
	wopt[1] = maxdlen >> 8;
	wopt[2] = sum;
	wopt[3] = sum >> 8;
	wopt[4] = maxdlen;
	wopt[5] = maxdlen >> 8;
	wopt[6] = maxdlen >> 16;
	wopt[7] = maxdlen >> 24;

	printf("- checksum: %x\n", ntohs(sum));

	woptlen = 8;

	if (community) {
	    wopt[8] = strlen(community);
	    memcpy((void *) &wopt[9], (void *)community, strlen(community));
	    woptlen += (1 + strlen(community));
	}
    } else { /* result & 32 */
	/* Firmware series 0.x.y.z - atsingle.bin */

	/* For 0.x.y.z firmware:
	 * - first 234 bytes from the file are used as auth. code
	 * - the actual firmware is being uploaded since offset 256 in the file
	 */

	i = read(fd, wopt, 234);
	if (i < 234) {
	    printf(ERR_READFW);
	    result = EXIT_FAILURE;
	    goto quit;
	}
	if (lseek(fd, 256, SEEK_SET) == -1) {
	    perror(_("Error while lseek()ing in firmware file\n"));
	    result = EXIT_FAILURE;
	    goto quit;
	}
	woptlen = 234;
	maxdlen = sb.st_size; /* no limit */
    }

    result = tftp(ap_ip.s_addr, remotefile, fd, wopt, woptlen, maxdlen);

quit:
    if (localfile)
	free(localfile);
    if (community)
	free(community);

    return(result);
}
