text: /*	$OpenBSD: connection.c,v 1.21 2015/12/05 06:38:18 mmcc Exp $ */
  
  /*
   * Copyright (c) 2009 Claudio Jeker <claudio@openbsd.org>
   *
   * Permission to use, copy, modify, and distribute this software for any
   * purpose with or without fee is hereby granted, provided that the above
   * copyright notice and this permission notice appear in all copies.
   *
   * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
   * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
   * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
   * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
   * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
   * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
   * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
   */
  
  #include <sys/types.h>
  #include <sys/queue.h>
  #include <sys/socket.h>
  #include <sys/uio.h>
  
  #include <netinet/in.h>
  #include <netinet/tcp.h>
  
  #include <scsi/iscsi.h>
  
  #include <errno.h>
  #include <event.h>
  #include <stdio.h>
  #include <stdlib.h>
  #include <string.h>
  #include <unistd.h>
  
  #include "iscsid.h"
  #include "log.h"
  
  void	conn_dispatch(int, short, void *);
  void	conn_write_dispatch(int, short, void *);
  
  int	c_do_connect(struct connection *, enum c_event);
  int	c_do_login(struct connection *, enum c_event);
  int	c_do_loggedin(struct connection *, enum c_event);
  int	c_do_req_logout(struct connection *, enum c_event);
  int	c_do_logout(struct connection *, enum c_event);
  int	c_do_loggedout(struct connection *, enum c_event);
  int	c_do_fail(struct connection *, enum c_event);
  int	c_do_cleanup(struct connection *, enum c_event);
  
  const char *conn_state(int);
  const char *conn_event(enum c_event);
  
  void
  conn_new(struct session *s, struct connection_config *cc)
  {
  	struct connection *c;
  	int nodelay = 1;
  
  	if (!(c = calloc(1, sizeof(*c))))
  		fatal("session_add_conn");
  
  	c->fd = -1;
  	c->state = CONN_FREE;
  	c->session = s;
  	c->cid = arc4random();
  	c->config = *cc;
  	c->mine = initiator_conn_defaults;
  	c->mine.HeaderDigest = s->config.HeaderDigest;
  	c->mine.DataDigest = s->config.DataDigest;
  	c->his = iscsi_conn_defaults;
  	c->active = iscsi_conn_defaults;
  
  	TAILQ_INIT(&c->pdu_w);
  	TAILQ_INIT(&c->tasks);
  	TAILQ_INSERT_TAIL(&s->connections, c, entry);
  
  	if (pdu_readbuf_set(&c->prbuf, PDU_READ_SIZE)) {
  		log_warn("conn_new");
  		conn_free(c);
  		return;
  	}
  
  	/* create socket */
  	c->fd = socket(c->config.TargetAddr.ss_family, SOCK_STREAM, 0);
  	if (c->fd == -1) {
  		log_warn("conn_new: socket");
  		conn_free(c);
  		return;
  	}
  	if (socket_setblockmode(c->fd, 1)) {
  		log_warn("conn_new: socket_setblockmode");
  		conn_free(c);
  		return;
  	}
  
  	/* try to turn off TCP Nagle */
  	if (setsockopt(c->fd, IPPROTO_TCP, TCP_NODELAY, &nodelay,
  	    sizeof(nodelay)) == -1)
  		log_warn("conn_new: setting TCP_NODELAY");
  
  	event_set(&c->ev, c->fd, EV_READ|EV_PERSIST, conn_dispatch, c);
  	event_set(&c->wev, c->fd, EV_WRITE, conn_write_dispatch, c);
  
  	conn_fsm(c, CONN_EV_CONNECT);
  }
  
  void
  conn_free(struct connection *c)
  {
  	log_debug("conn_free");
  
  	pdu_readbuf_free(&c->prbuf);
  	pdu_free_queue(&c->pdu_w);
  
  	event_del(&c->ev);
  	event_del(&c->wev);
  	if (c->fd != -1)
  		close(c->fd);
  
  	taskq_cleanup(&c->tasks);
  
  	TAILQ_REMOVE(&c->session->connections, c, entry);
  	free(c);
  }
  
  void
  conn_dispatch(int fd, short event, void *arg)
  {
  	struct connection *c = arg;
  	ssize_t n;
  
  	if (!(event & EV_READ)) {
  		log_debug("spurious read call");
  		return;
  	}
  	if ((n = pdu_read(c)) == -1) {
  		if (errno == EAGAIN || errno == ENOBUFS ||
  		    errno == EINTR)	/* try later */
  			return;
  		log_warn("pdu_read");
  		conn_fsm(c, CONN_EV_FAIL);
  		return;
  	}
  	if (n == 0) {    /* connection closed */
  		conn_fsm(c, CONN_EV_CLOSED);
  		return;
  	}
  
  	pdu_parse(c);
  }
  
  void
  conn_write_dispatch(int fd, short event, void *arg)
  {
  	struct connection *c = arg;
  	ssize_t n;
  	int error;
  	socklen_t len;
  
  	if (!(event & EV_WRITE)) {
  		log_debug("spurious write call");
  		return;
  	}
  
  	switch (c->state) {
  	case CONN_XPT_WAIT:
  		len = sizeof(error);
  		if (getsockopt(c->fd, SOL_SOCKET, SO_ERROR,
  		    &error, &len) == -1 || (errno = error)) {
  			log_warn("connect to %s failed",
  			    log_sockaddr(&c->config.TargetAddr));
  			conn_fsm(c, CONN_EV_FAIL);
  			return;
  		}
  		conn_fsm(c, CONN_EV_CONNECTED);
  		break;
  	default:
  		if ((n = pdu_write(c)) == -1) {
  			log_warn("pdu_write");
  			conn_fsm(c, CONN_EV_FAIL);
  			return;
  		}
  		if (n == 0) {    /* connection closed */
  			conn_fsm(c, CONN_EV_CLOSED);
  			return;
  		}
  
  		/* check if there is more to send */
  		if (pdu_pending(c))
  			event_add(&c->wev, NULL);
  	}
  }
  
  void
  conn_fail(struct connection *c)
  {
  	log_debug("conn_fail");
  	conn_fsm(c, CONN_EV_FAIL);
  }
  
  int
  conn_task_ready(struct connection *c)
  {
  	if ((c->state & CONN_RUNNING) && TAILQ_EMPTY(&c->tasks))
  		return 1;
  	return 0;
  }
  
  void
  conn_task_issue(struct connection *c, struct task *t)
  {
  	TAILQ_INSERT_TAIL(&c->tasks, t, entry);
  	conn_task_schedule(c);
  }
  
  void
  conn_task_schedule(struct connection *c)
  {
  	struct task *t = TAILQ_FIRST(&c->tasks);
  	struct pdu *p, *np;
  
  	if (!t) {
  		log_debug("conn_task_schedule: task is hiding");
  		return;
  	}
  
  	/* move pdus to the write queue */
  	for (p = TAILQ_FIRST(&t->sendq); p != NULL; p = np) {
  		np = TAILQ_NEXT(p, entry);
  		TAILQ_REMOVE(&t->sendq, p, entry);
  		conn_pdu_write(c, p);
  	}
  	if (t->callback == NULL) {
  		/* no callback, immediate command expecting no answer */
  		conn_task_cleanup(c, t);
  		free(t);
  	}
  }
  
  void
  conn_task_cleanup(struct connection *c, struct task *t)
  {
  	pdu_free_queue(&t->sendq);
  	pdu_free_queue(&t->recvq);
  	/* XXX need some state to know if queued or not */
  	if (c) {
  		TAILQ_REMOVE(&c->tasks, t, entry);
  		if (!TAILQ_EMPTY(&c->tasks))
  			conn_task_schedule(c);
  		else
  			session_schedule(c->session);
  	}
  }
  
  #define SET_NUM(p, x, v, min, max)				\
  do {								\
  	if (!strcmp((p)->key, #v)) {				\
  		(x)->his.v = text_to_num((p)->value, (min), (max), &err); \
  		if (err) {					\
  			log_warnx("bad param %s=%s: %s",	\
  			    (p)->key, (p)->value, err);		\
  			errors++;				\
  		}						\
  log_debug("SET_NUM: %s = %llu", #v, (u_int64_t)(x)->his.v);	\
  	}							\
  } while (0)
  
  #define SET_BOOL(p, x, v)					\
  do {								\
  	if (!strcmp((p)->key, #v)) {				\
  		(x)->his.v = text_to_bool((p)->value, &err);	\
  		if (err) {					\
  			log_warnx("bad param %s=%s: %s",	\
  			    (p)->key, (p)->value, err);		\
  			errors++;				\
  		}						\
  log_debug("SET_BOOL: %s = %u", #v, (int)(x)->his.v);		\
  	}							\
  } while (0)
  
  int
  conn_parse_kvp(struct connection *c, struct kvp *kvp)
  {
  	struct kvp *k;
  	struct session *s = c->session;
  	const char *err;
  	int errors = 0;
  
  
  	for (k = kvp; k->key; k++) {
  log_debug("conn_parse_kvp: %s = %s", k->key, k->value);
  		/* XXX handle NotUnderstood|Irrelevant|Reject */
  		SET_NUM(k, s, MaxBurstLength, 512, 16777215);
  		SET_NUM(k, s, FirstBurstLength, 512, 16777215);
  		SET_NUM(k, s, DefaultTime2Wait, 0, 3600);
  		SET_NUM(k, s, DefaultTime2Retain, 0, 3600);
  		SET_NUM(k, s, MaxOutstandingR2T, 1, 65535);
  		SET_NUM(k, s, TargetPortalGroupTag, 0, 65535);
  		SET_NUM(k, s, MaxConnections, 1, 65535);
  		SET_BOOL(k, s, InitialR2T);
  		SET_BOOL(k, s, ImmediateData);
  		SET_BOOL(k, s, DataPDUInOrder);
  		SET_BOOL(k, s, DataSequenceInOrder);
  		SET_NUM(k, s, ErrorRecoveryLevel, 0, 2);
  		SET_NUM(k, c, MaxRecvDataSegmentLength, 512, 16777215);
  	}
  
  	if (errors) {
  		log_warnx("conn_parse_kvp: errors found");
  		return -1;
  	}
  	return 0;
  }
  
  #undef SET_NUM
  #undef SET_BOOL
  
  int
  conn_gen_kvp(struct connection *c, struct kvp *kvp, size_t *nkvp)
  {
  	struct session *s = c->session;
  	size_t i = 0;
  
  	if (s->mine.MaxConnections != iscsi_sess_defaults.MaxConnections) {
  		if (kvp && i < *nkvp) {
  			kvp[i].key = strdup("MaxConnections");
  			if (kvp[i].key == NULL)
  				return -1;
  			if (asprintf(&kvp[i].value, "%hu",
  			    s->mine.MaxConnections) == -1) {
  				kvp[i].value = NULL;
  				return -1;
  			}
  		}
  		i++;
  	}
  	if (c->mine.MaxRecvDataSegmentLength !=
  	    iscsi_conn_defaults.MaxRecvDataSegmentLength) {
  		if (kvp && i < *nkvp) {
  			kvp[i].key = strdup("MaxRecvDataSegmentLength");
  			if (kvp[i].key == NULL)
  				return -1;
  			if (asprintf(&kvp[i].value, "%u",
  			    c->mine.MaxRecvDataSegmentLength) == -1) {
  				kvp[i].value = NULL;
  				return -1;
  			}
  		}
  		i++;
  	}
  
  	*nkvp = i;
  	return 0;
  }
  
  void
  conn_pdu_write(struct connection *c, struct pdu *p)
  {
  	struct iscsi_pdu *ipdu;
  
  /* XXX I GUESS THIS SHOULD BE MOVED TO PDU SOMEHOW... */
  	ipdu = pdu_getbuf(p, NULL, PDU_HEADER);
  	switch (ISCSI_PDU_OPCODE(ipdu->opcode)) {
  	case ISCSI_OP_I_NOP:
  	case ISCSI_OP_SCSI_REQUEST:
  	case ISCSI_OP_TASK_REQUEST:
  	case ISCSI_OP_LOGIN_REQUEST:
  	case ISCSI_OP_TEXT_REQUEST:
  	case ISCSI_OP_DATA_OUT:
  	case ISCSI_OP_LOGOUT_REQUEST:
  	case ISCSI_OP_SNACK_REQUEST:
  		ipdu->expstatsn = ntohl(c->expstatsn);
  		break;
  	}
  
  	TAILQ_INSERT_TAIL(&c->pdu_w, p, entry);
  	event_add(&c->wev, NULL);
  }
  
  /* connection state machine more or less as specified in the RFC */
  struct {
  	int		state;
  	enum c_event	event;
  	int		(*action)(struct connection *, enum c_event);
  } fsm[] = {
  	{ CONN_FREE, CONN_EV_CONNECT, c_do_connect },		/* T1 */
  	{ CONN_XPT_WAIT, CONN_EV_CONNECTED, c_do_login },	/* T4 */
  	{ CONN_IN_LOGIN, CONN_EV_LOGGED_IN, c_do_loggedin },	/* T5 */
  	{ CONN_LOGGED_IN, CONN_EV_LOGOUT, c_do_logout },	/* T9 */
  	{ CONN_LOGGED_IN, CONN_EV_REQ_LOGOUT, c_do_req_logout },/* T11 */
  	{ CONN_LOGOUT_REQ, CONN_EV_LOGOUT, c_do_logout },	/* T10 */
  	{ CONN_LOGOUT_REQ, CONN_EV_REQ_LOGOUT, c_do_req_logout},/* T12 */
  	{ CONN_LOGOUT_REQ, CONN_EV_LOGGED_OUT, c_do_loggedout },/* T18 */
  	{ CONN_IN_LOGOUT, CONN_EV_LOGGED_OUT, c_do_loggedout },	/* T13 */
  	{ CONN_IN_LOGOUT, CONN_EV_REQ_LOGOUT, c_do_req_logout },/* T14 */
  	{ CONN_CLEANUP_WAIT, CONN_EV_CLEANING_UP, c_do_cleanup},/* M2 */
  	{ CONN_CLEANUP_WAIT, CONN_EV_FREE, c_do_loggedout },	/* M1 */
  	{ CONN_IN_CLEANUP, CONN_EV_FREE, c_do_loggedout },	/* M4 */
  	{ CONN_IN_CLEANUP, CONN_EV_CLEANING_UP, c_do_cleanup},
  	/* either one of T2, T7, T15, T16, T17, M3 */
  	{ CONN_ANYSTATE, CONN_EV_CLOSED, c_do_fail },
  	{ CONN_ANYSTATE, CONN_EV_FAIL, c_do_fail },
  	{ CONN_ANYSTATE, CONN_EV_FREE, c_do_fail },
  	{ 0, 0, NULL }
  };
  
  void
  conn_fsm(struct connection *c, enum c_event event)
  {
  	int	i, ns;
  
  	for (i = 0; fsm[i].action != NULL; i++) {
  		if (c->state & fsm[i].state && event == fsm[i].event) {
  			log_debug("conn_fsm[%s]: %s ev %s",
  			    c->session->config.SessionName,
  			    conn_state(c->state), conn_event(event));
  			ns = fsm[i].action(c, event);
  			if (ns == -1)
  				/* XXX better please */
  				fatalx("conn_fsm: action failed");
  			log_debug("conn_fsm[%s]: new state %s",
  			    c->session->config.SessionName, conn_state(ns));
  			c->state = ns;
  			return;
  		}
  	}
  	log_warnx("conn_fsm[%s]: unhandled state transition [%s, %s]",
  	    c->session->config.SessionName, conn_state(c->state),
  	    conn_event(event));
  	fatalx("bork bork bork");
  }
  
  int
  c_do_connect(struct connection *c, enum c_event ev)
  {
  	if (c->fd == -1) {
  		log_warnx("connect(%s), lost socket",
  		    log_sockaddr(&c->config.TargetAddr));
  		session_fsm(c->session, SESS_EV_CONN_FAIL, c, 0);
  		return CONN_FREE;
  	}
  	if (c->config.LocalAddr.ss_len != 0) {
  		if (bind(c->fd, (struct sockaddr *)&c->config.LocalAddr,
  		    c->config.LocalAddr.ss_len) == -1) {
  			log_warn("bind(%s)",
  			    log_sockaddr(&c->config.LocalAddr));
  			session_fsm(c->session, SESS_EV_CONN_FAIL, c, 0);
  			return CONN_FREE;
  		}
  	}
  	if (connect(c->fd, (struct sockaddr *)&c->config.TargetAddr,
  	    c->config.TargetAddr.ss_len) == -1) {
  		if (errno == EINPROGRESS) {
  			event_add(&c->wev, NULL);
  			event_add(&c->ev, NULL);
  			return CONN_XPT_WAIT;
  		} else {
  			log_warn("connect(%s)",
  			    log_sockaddr(&c->config.TargetAddr));
  			session_fsm(c->session, SESS_EV_CONN_FAIL, c, 0);
  			return CONN_FREE;
  		}
  	}
  	event_add(&c->ev, NULL);
  	/* move forward */
  	return c_do_login(c, CONN_EV_CONNECTED);
  }
  
  int
  c_do_login(struct connection *c, enum c_event ev)
  {
  	/* start a login session and hope for the best ... */
  	initiator_login(c);
  	return CONN_IN_LOGIN;
  }
  
  int
  c_do_loggedin(struct connection *c, enum c_event ev)
  {
  	iscsi_merge_conn_params(&c->active, &c->mine, &c->his);
  	session_fsm(c->session, SESS_EV_CONN_LOGGED_IN, c, 0);
  
  	return CONN_LOGGED_IN;
  }
  
  int
  c_do_req_logout(struct connection *c, enum c_event ev)
  {
  	/* target requested logout. XXX implement async handler */
  
  	if (c->state & CONN_IN_LOGOUT)
  		return CONN_IN_LOGOUT;
  	else
  		return CONN_LOGOUT_REQ;
  }
  
  int
  c_do_logout(struct connection *c, enum c_event ev)
  {
  	/* logout is in progress ... */
  	return CONN_IN_LOGOUT;
  }
  
  int
  c_do_loggedout(struct connection *c, enum c_event ev)
  {
  	/*
  	 * Called by the session fsm before calling conn_free.
  	 * Doing this so the state transition is logged.
  	 */
  	return CONN_FREE;
  }
  
  int
  c_do_fail(struct connection *c, enum c_event ev)
  {
  	log_debug("c_do_fail");
  
  	/* cleanup events so that the connection does not retrigger */
  	event_del(&c->ev);
  	event_del(&c->wev);
  	close(c->fd);
  	c->fd = -1;	/* make sure this fd is not closed again */
  
  	/* all pending task have failed so clean them up */
  	taskq_cleanup(&c->tasks);
  
  	/* session will take care of cleaning up the mess */
  	session_fsm(c->session, SESS_EV_CONN_FAIL, c, 0);
  
  	if (ev == CONN_EV_FREE || c->state & CONN_NEVER_LOGGED_IN)
  		return CONN_FREE;
  	return CONN_CLEANUP_WAIT;
  }
  
  int
  c_do_cleanup(struct connection *c, enum c_event ev)
  {
  	/* nothing to do here just adjust state */
  	return CONN_IN_CLEANUP;
  }
  
  const char *
  conn_state(int s)
  {
  	static char buf[15];
  
  	switch (s) {
  	case CONN_FREE:
  		return "FREE";
  	case CONN_XPT_WAIT:
  		return "XPT_WAIT";
  	case CONN_XPT_UP:
  		return "XPT_UP";
  	case CONN_IN_LOGIN:
  		return "IN_LOGIN";
  	case CONN_LOGGED_IN:
  		return "LOGGED_IN";
  	case CONN_IN_LOGOUT:
  		return "IN_LOGOUT";
  	case CONN_LOGOUT_REQ:
  		return "LOGOUT_REQ";
  	case CONN_CLEANUP_WAIT:
  		return "CLEANUP_WAIT";
  	case CONN_IN_CLEANUP:
  		return "IN_CLEANUP";
  	default:
  		snprintf(buf, sizeof(buf), "UKNWN %x", s);
  		return buf;
  	}
  	/* NOTREACHED */
  }
  
  const char *
  conn_event(enum c_event e)
  {
  	static char buf[15];
  
  	switch (e) {
  	case CONN_EV_FAIL:
  		return "fail";
  	case CONN_EV_CONNECT:
  		return "connect";
  	case CONN_EV_CONNECTED:
  		return "connected";
  	case CONN_EV_LOGGED_IN:
  		return "logged in";
  	case CONN_EV_REQ_LOGOUT:
  		return "logout requested";
  	case CONN_EV_LOGOUT:
  		return "logout";
  	case CONN_EV_LOGGED_OUT:
  		return "logged out";
  	case CONN_EV_CLEANING_UP:
  		return "cleaning up";
  	case CONN_EV_CLOSED:
  		return "closed";
  	case CONN_EV_FREE:
  		return "forced free";
  	}
  
  	snprintf(buf, sizeof(buf), "UKNWN %d", e);
  	return buf;
  }
  