/*-
 * Copyright (c) 2003 Robert N. M. Watson
 * 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 THE 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 THE 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.
 *
 * $FreeBSD$
 */
#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/queue.h>
#include <sys/socket.h>
#include <sys/sysctl.h>
#include <sys/systm.h>

#include <net/ethernet.h>
#include <net/if.h>
#include <net/if_types.h>
#include <net/if_var.h>

#include <dev/ethercons/ethercons.h>

SYSCTL_NODE(_kern, OID_AUTO, ethercons, CTLFLAG_RW, 0, "ethercons controls");

static const u_char ethercons_default_target[ETHER_ADDR_LEN] =
    {0xff, 0xff, 0xff, 0xff, 0xff, 0xff};
static u_char ethercons_current_target[ETHER_ADDR_LEN];

static int	ethercons_up;
static u_int	ethercons_bytestried;
static u_int	ethercons_bytessent;
static int	ethercons_lasterror;
static int	ethercons_lastsize;

SYSCTL_UINT(_kern_ethercons, OID_AUTO, bytestried, CTLFLAG_RD,
    &ethercons_bytestried, 0, "");
SYSCTL_UINT(_kern_ethercons, OID_AUTO, bytessent, CTLFLAG_RD,
    &ethercons_bytessent, 0, "");
SYSCTL_UINT(_kern_ethercons, OID_AUTO, lasterror, CTLFLAG_RD,
    &ethercons_lasterror, 0, "");
SYSCTL_UINT(_kern_ethercons, OID_AUTO, lastsize, CTLFLAG_RD,
    &ethercons_lastsize, 0, "");

static char	ethercons_interface[128];
static char	ethercons_interface_preference[128];
static char	ethercons_target[128];
static int	ethercons_ifnet_raise;

SYSCTL_STRING(_kern_ethercons, OID_AUTO, interface, CTLFLAG_RD,
    ethercons_interface, 0, "");
SYSCTL_STRING(_kern_ethercons, OID_AUTO, interface_preference, CTLFLAG_RW,
    ethercons_interface_preference, 128, "");
TUNABLE_STR("kern.ethercons.interface_preference",
    ethercons_interface_preference, sizeof(ethercons_interface_preference));
TUNABLE_STR("kern.ethercons.target", ethercons_target,
    sizeof(ethercons_target));
TUNABLE_INT("kern.ethercons.ifnet_raise", &ethercons_ifnet_raise);

/*
 * Parse an ethernet address from a string; used for the boot-time tunable
 * to select a target ethernet address for console output, and at run-time
 * for sysctl changes to that address.
 */
static int
parse_ethernet_addr(char *string, u_char *target_addr)
{
	u_char temp_addr[ETHER_ADDR_LEN];
	char *cp, *endp;
	long val;
	int i;

	i = 0;
	while ((cp = strsep(&string, ":")) != NULL) {
		if (cp[0] == '\0')
			return (EINVAL);
		val = strtol(cp, &endp, 16);
		if (*endp != '\0' || val < 0 || val > 255)
			return (EINVAL);
		temp_addr[i] = val;	
		i++;
		if (i > ETHER_ADDR_LEN)
			return (EINVAL);
	}
	if (i != ETHER_ADDR_LEN)
		return (EINVAL);
	bcopy(temp_addr, target_addr, ETHER_ADDR_LEN);
	return (0);
}

/*
 * Because consoles are initialized very early, before even interface
 * locks are initialized, we need our own "ready to start" flag.  This
 * will prevent us from trying to interact with the network stack before
 * it is ready.  Potentially should be SI_SUB_PROTO_END.  Also, pull
 * in any default target information: first, grab the compile-time
 * default, then overlay with a boot-time default if it's present and
 * parseable.
 */
static void
ethercons_init(void *arg)
{

	bcopy(ethercons_default_target, ethercons_current_target,
	    ETHER_ADDR_LEN);
	if (ethercons_target[0] != '\0')
		(void)parse_ethernet_addr(ethercons_target,
		    ethercons_current_target);
	ethercons_up = 1;
}
SYSINIT(ethercons_init, SI_SUB_INIT_IF, SI_ORDER_ANY, ethercons_init, NULL);

/*
 * If a preference is present in the tunable, try that first, otherwise
 * pick the first, otherwise fail.  Update a sysctl variable so that
 * the user can see what the most recent preference was, if any.
 */
static struct ifnet *
ethercons_find_ifnet(void)
{
	struct ifnet *ifp;
	char ifname[IFNAMSIZ];

	ifp = NULL;
	IFNET_RLOCK();
	if (ethercons_interface_preference[0] != '\0') {
		TAILQ_FOREACH(ifp, &ifnet, if_link) {
			snprintf(ifname, IFNAMSIZ, "%s%d", ifp->if_name,
			    ifp->if_unit);
			if (strcmp(ifname,
			    ethercons_interface_preference) == 0)
				break;
		}
	}
	if (ifp == NULL) {
		TAILQ_FOREACH(ifp, &ifnet, if_link) {
			if (ifp->if_type == IFT_ETHER)
				break;
		}
	}
	IFNET_RUNLOCK();
	if (ifp != NULL) {
		snprintf(ifname, IFNAMSIZ, "%s%d", ifp->if_name, ifp->if_unit);
		strcpy(ethercons_interface, ifname);
	} else
		ethercons_interface[0] = '\0';

	return (ifp);
}

/*
 * Optionally bring the interface of choice up at boot before hitting
 * userspace.  This allows ethercons to be used with single-user mode.
 * Do this sometime around the storage system coming online.
 */
static void
ethercons_raise(void *arg)
{
	struct ifnet *ifp;

	if (!ethercons_ifnet_raise)
		return;
	ifp = ethercons_find_ifnet();
	if (ifp != NULL)
		if_up(ifp);
	else
		printf("ethercons_raise: flag set but no ifnet\n");
}
SYSINIT(ethercons_raise, SI_SUB_ROOT_CONF, SI_ORDER_FIRST, ethercons_raise,
    NULL);

/*
 * Given a nul-terminate console string, send the string (and the nul)
 * out as a packet.  Internal transmission limit is the minimum of 1024
 * bytes and the mtu.
 */
void
ethercons_transmit(int len, char *buffer)
{
	struct ethercons_header *ech;
	struct mbuf *m, *top, **mp;
	struct ether_header *eh;
	int error, mlen, tlen;
	struct sockaddr sa;
	struct ifnet *ifp;
	char *cp;
	int slen;

	/*
	 * Potentially, if not up, or no interface, we may want to buffer
	 * here and replay later driven by some event or another.
	 */
	if (!ethercons_up) {
		ethercons_lasterror = ENETDOWN;
		return;
	}

	ifp = ethercons_find_ifnet();
	if (ifp == NULL) {
		ethercons_lasterror = ENETDOWN;
		return;
	}
	KASSERT(ifp->if_type == IFT_ETHER,
	    ("ethercons_send_bytes: not an ethernet interface"));

	slen = len + 1;
	ethercons_bytestried += slen;
	if (slen + sizeof(struct ether_header) > 1024 ||
	    slen + sizeof(struct ether_header) > ifp->if_mtu) {
		ethercons_lasterror = EMSGSIZE;
		return;
	}

	MGETHDR(m, M_DONTWAIT, MT_DATA);
	if (m == NULL) {
		ethercons_lasterror = EMSGSIZE;
		return;
	}

	/*
	 * First build an mbuf chain holding the string passed by the
	 * console code.
	 */
	cp = buffer;
	tlen = slen;
	mlen = MHLEN;
	top = NULL;
	mp = &top;
	error = 0;
	while (error == 0 && tlen > 0) {
		m->m_len = min(mlen, tlen);
		bcopy(cp, mtod(m, char *), m->m_len);
		*mp = m;
		mp = &m->m_next;
		cp += m->m_len;
		tlen -= m->m_len;
		if (tlen > 0) {
			MGET(m, M_DONTWAIT, MT_DATA);
			if (m == NULL) {
				error = ENOBUFS;
				break;
			}
			mlen = MLEN;
		}
	}
	if (error) {
		if (top != NULL)
			m_freem(top);
		ethercons_lasterror = error;
		return;
	}
	m = top;
	m->m_pkthdr.len = slen;

	/*
	 * Now append the ethercons_header.
	 */
	m = m_prepend(m, sizeof(struct ethercons_header), M_DONTWAIT);
	if (m == NULL) {
		ethercons_lasterror = ENOBUFS;
		return;
	}
	m = m_pullup(m, sizeof(struct ethercons_header));
	if (m == NULL) {
		ethercons_lasterror = ENOBUFS;
		return;
	}
	ech = mtod(m, struct ethercons_header *);
	ech->eh_version = htons(ETHERCONS_VERSION);
	ech->eh_flags = 0;

	m->m_pkthdr.rcvif = ifp;
#ifdef MAC
	mac_create_mbuf_ethercons(ifp, m);
#endif

	sa.sa_family = AF_UNSPEC;
	sa.sa_len = sizeof(sa);
	eh = (struct ether_header *)sa.sa_data;
	eh->ether_type = htons(ETHERTYPE_ETHERCONS);
	bcopy(ethercons_current_target, &eh->ether_dhost,
	    sizeof(eh->ether_dhost));

	error = (*ifp->if_output)(ifp, m, &sa, NULL);
	if (error == 0) {
		ethercons_bytessent += slen;
		ethercons_lastsize = slen;
	}
	ethercons_lasterror = error;
}

void
ethercons_input(struct mbuf *m)
{
	struct ethercons_header *ech;
	char *c;

	/*
	 * If it's the wrong version of the ethercons protocol, or if it's
	 * not an input packet, ignore.  Leave enough room for at least
	 * a nul-termination.
	 */
	if (m->m_flags & (M_BCAST | M_MCAST)) {
		m_freem(m);
		return;
	}
	m = m_pullup(m, sizeof(*ech) + 1);
	if (m == NULL)
		return;
	ech = mtod(m, struct ethercons_header *);
	if (ntohs(ech->eh_version) != ETHERCONS_VERSION) {
		m_freem(m);
		return;
	}
	if ((ntohs(ech->eh_flags) & ETHERCONS_FLAG_INPUT) == 0) {
		m_freem(m);
		return;
	}
	m_adj(m, sizeof(*ech));

	/*
	 * Spit it into the ring buffer one character at a time.  If we
	 * hit a nul-termination, stop.
	 */
	while (m->m_pkthdr.len > 0) {
		m = m_pullup(m, 1);
		if (m == NULL)
			return;
		c = mtod(m, char *);
		if (*c == '\0')
			break;
		ethercons_tty_intr(*c);
		m_adj(m, 1);
	}
	m_freem(m);
}

#define	ETHER_ADDRESS_MAX_STRING	(2*6+5+1)
static int
sysctl_ethercons_target(SYSCTL_HANDLER_ARGS)
{
	char buf[ETHER_ADDRESS_MAX_STRING];
	int error;

	snprintf(buf, ETHER_ADDRESS_MAX_STRING,
	    "%02x:%02x:%02x:%02x:%02x:%02x", ethercons_current_target[0],
	    ethercons_current_target[1], ethercons_current_target[2],
	    ethercons_current_target[3], ethercons_current_target[4],
	    ethercons_current_target[5]);
	error = sysctl_handle_string(oidp, buf, ETHER_ADDRESS_MAX_STRING, req);
	if (error == 0 && req->newptr != NULL)
		error = parse_ethernet_addr(buf, ethercons_current_target);
	return (error);
}

SYSCTL_PROC(_kern_ethercons, OID_AUTO, target, CTLTYPE_STRING|CTLFLAG_RW,
    NULL, 0, sysctl_ethercons_target, "A",
    "Target ethernet address for ethercons");
