/*****************************************************************************\
* Copyright (c) 2002-2003 Pelle Johansson.                                    *
* All rights reserved.                                                        *
*                                                                             *
* This file is part of the moftpd package. Use and distribution of            *
* this software is governed by the terms in the file LICENCE, which           *
* should have come with this package.                                         *
\*****************************************************************************/

/* $moftpd: connection.c 1245 2004-12-09 12:01:59Z morth $ */

#include "system.h"

#include "connection.h"

#include "server.h"
#include "main.h"
#include "commands.h"
#include "utf8fs/file.h"
#include "utf8fs/memory.h"
#include "accounter.h"
#include "events.h"
#include "confparse.h"

static void *connections;

extern int reloadConfig, urgData, debug;
extern const char *localeDir, *localeSuffix;

static int accounter_reply (int sock, void *user, int urgent)
{
  connection_t *conn = user;
  int l;
  char buf[4097], *bp, *nbp, *str;
#ifdef HAVE_LIBPAM
  static struct pam_conv conv = { conv_fun };
#endif
  char rhost[NI_MAXHOST], lhost[NI_MAXHOST], rport[NI_MAXSERV], lport[NI_MAXSERV];
  struct sockaddr_storage lAddr, rAddr;
  
  if (conn->sock == -1)
    return 0;
  
  l = read (sock, buf, sizeof (buf) - 1);
  if (l <= 0)
  {
    if (l < 0 && errno == EINTR)
      return 0;
    close (conn->accSock);
    conn->accSock = -1;
    if (conn->accWait)
      reply (conn, "500 Accounter error.");
    close_data_connection (conn);
    return 0;
  }
  buf[l] = 0;
  
  for (bp = buf; bp; bp = nbp)
  {
    nbp = strchr (bp, '\n');
    if (nbp)
      *nbp++ = 0;
    
    if (!bp[0])
      continue;
    
    if (!strncmp (bp, "MSG ", 4))
      reply_spont (conn, bp + 4, 0);
    else if (!strncmp (bp, "ABORT", 5))
    {
      if (conn->working)
      {
	l = close_data_connection (conn);
	if (l == -1)
	  return 0;
	if (bp[5] && bp[6])
	  reply (conn, "450 %s", bp + 6);
	else
	  reply (conn, "450 Transfer aborted by administrator.");
      }
    }
    else if (!strncmp (bp, "DISCONNECT", 10))
    {
      if (bp[10] && bp[11] && (str = talloc (strlen (bp + 11) + 4)))
      {
	strcpy (str, "421 ");
	strcat (str, bp + 11);
	reply_spont (conn, str, 1);
      }
      else
	reply_spont (conn, "421 Connection closed by administrator.", 1);
    }
    else if (!strcmp (bp, "RELOAD"))
      ; // Just ignore.
    else
    {
      switch (conn->accWait)
      {
      case acWelcome:
	l = sizeof(lAddr);
	getsockname(conn->sock, (struct sockaddr*)&lAddr, &l);
	if (getnameinfo ((struct sockaddr*)&lAddr, l, lhost, sizeof (lhost), lport,
		  sizeof (lport), NI_NUMERICHOST | NI_NUMERICSERV))
	{
	  strcpy (lhost, "unknown");
	  strcpy (lport, "0");
	}
	l = sizeof(rAddr);
	getpeername(conn->sock, (struct sockaddr*)&rAddr, &l);
	if (getnameinfo ((struct sockaddr*)&rAddr, l, rhost, sizeof (rhost), rport,
		  sizeof (rport), NI_NUMERICHOST | NI_NUMERICSERV))
	{
	  strcpy (rhost, "unknown");
	  strcpy (rport, "0");
	}
	
	l = -1;
	if (sscanf (bp, "ALLOW %d", &l) == 1)
	{
#ifdef USE_SQL
	  if (conn->server->sql.type && conn->server->sqlConnectQuery)
	  {
	    if (sql_connect (&conn->server->sql, conn->server->sqlHost,
		      conn->server->sqlUser, conn->server->sqlDB,
		      conn->server->sqlPass, conn->server->sqlCert,
		      conn->server->sqlKey))
	      l = -2;
	    else
	    {
	      sql_arg_t args[5];
	      int res;
	      
	      args[0].ch = 's';
	      args[0].str = tstring (sql_quote (conn->server->name));
	      args[1].ch = 'l';
	      args[1].str = tstring (sql_quote (lhost));
	      args[2].ch = 'r';
	      args[2].str = tstring (sql_quote (rhost));
	      args[3].ch = 'L';
	      args[3].str = lport;
	      args[4].ch = 'R';
	      args[4].str = rport;
	      res = sql_query (&conn->server->sql, conn->server->sqlConnectQuery, 5, args);
	      if (res <= 0)
		l = -2;
	      else
	      {
		const char *field, *val;
		int b;
		
		for (res = 0; (field = sql_fetch_cell (&conn->server->sql, -1, res)); res++)
		{
		  val = sql_fetch_cell (&conn->server->sql, 0, res);
		  if (!val)
		    continue;
		  
		  if (!strcasecmp (field, "ALLOW"))
		  {
		    b = parse_bool (val);
		    if (!b)
		    {
		      l = -2;
		      break;
		    }
		  }
		  else if (!strcasecmp (field, "LANG"))
		  {
		    str = talloc (strlen (localeDir) + strlen (val) + strlen (localeSuffix) + 1);
		    if (str)
		    {
		      strcpy (str, localeDir);
		      strcat (str, val);
		      strcat (str, localeSuffix);
		      b = open_shared (str);
		      if (b >= 0)
		      {
			if (conn->langFd >= 0)
			  close_shared (conn->langFd);
			conn->langFd = b;
			pfree (conn->currLang, conn);
			conn->currLang = pstring (val, conn);
		      }
		    }
		  }
		}
	      }
	      sql_free_result (&conn->server->sql);
	      if (l < 0 && !conn->server->sqlRefs)
		sql_disconnect (&conn->server->sql);
	    }
	  }
#endif
	}
	if (l >= 0)
	{
	  conn->id = l;
#ifdef HAVE_LIBPAM
	  if (conn->pamh)
	    pam_end (conn->pamh, PAM_SUCCESS);
	  conv.appdata_ptr = conn;
	  l = pam_start (conn->server->pam_service, NULL, &conv, &conn->pamh);
	  if (l != PAM_SUCCESS)
	  {
	    reply (conn, "421 %s.", pam_strerror (conn->pamh, l));
	    disconnected (conn);
	    return 0;
	  }
#ifdef PAM_TTY
	  pam_set_item (conn->pamh, PAM_TTY, conn->server->pam_service);
#endif
#ifdef PAM_RHOST
	  if (strcmp (rhost, "unknown"))
	    pam_set_item (conn->pamh, PAM_RHOST, rhost);
#endif
#endif
	  if (conn->oldserver)
	  {
#ifdef USE_SQL
	    if (!conn->oldserver->sqlRefs)
	      sql_disconnect (&conn->oldserver->sql);
#endif
	    pfree (conn->oldserver, conn);
	    conn->oldserver = NULL;
	  }
	  else
	  {
	    syslog (LOG_INFO, "Connection to %s from %s (id: %d)", conn->server->name, rhost, conn->id);
	    // Start listening for commands.
	    add_read_fd (conn->sock, control_handler, conn);
	  }
	  
	  if (conn->server->welcomeMsg)
	    reply_msg (conn, "220 %s", conn->server->welcomeMsg);
	  else
	  {
	    if (find_server ((struct sockaddr*)&lAddr, (struct sockaddr*)&rAddr, conn->server))
	    {
	      reply (conn, "220-Service available at %s (%s).", conn->server->name, PACKAGE_STRING);
	      reply (conn, "220 You can use the HOST command to switch to other servers.");
	    }
	    else
	      reply (conn, "220 Service available at %s (%s).", conn->server->name, PACKAGE_STRING);
	  }
	}
	else
	{
	  if (l < -1 || !strcmp (bp, "DENY"))
	  {
	    pfree (conn->server, conn);
	    if (conn->oldserver)
	    {
	      conn->server = conn->oldserver;
	      conn->oldserver = NULL;
	      if (l < -1)
		reply (conn, "500 Access denied.");
	      else
		reply (conn, "500 Connection limit reached.");
	      break;
	    }
	    conn->server = pattach (find_server ((struct sockaddr*)&lAddr, (struct sockaddr*)&rAddr, conn->server), conn);
	    if (conn->server)
	    {
	      if (accounter (conn->accSock, "CONNECT %s %s\n", rhost, conn->server->name))
	      {
		syslog (LOG_ERR, "Connect from %s: Error in acocunter: %m", rhost);
		reply (conn, "421 Error: %s. Please try again later.", strerror (errno));
		disconnected (conn);
		return 0;
	      }
	      continue;
	    }
	    else
	    {
	      if (l < -1)
	      {
		reply (conn, "421 No service available at this address.");
		syslog (LOG_NOTICE, "Connect from %s: Denied by rule.", rhost);
	      }
	      else
	      {
		reply (conn, "421 Connection limit reached.");
		syslog (LOG_NOTICE, "Connect from %s: Connection limit reached.", rhost);
	      }
	      disconnected (conn);
	      return 0;
	    }
	  }
	  else
	  {
	    if (conn->oldserver)
	    {
	      pfree (conn->server, conn);
	      conn->server = conn->oldserver;
	      conn->oldserver = NULL;
	      reply (conn, "500 Temporary error.");
	      break;
	    }
	    reply (conn, "421 Temporary error. Please try again later.");
	    syslog (LOG_ERR, "Connect from %s: Error in acocunter reply: %s", rhost, buf);
	    disconnected (conn);
	    return 0;
	  }
	}
	break;
      case acLogin:
	if (!strcmp (bp, "ALLOW"))
	{
	  if (authenticated (conn))
	  {
	    unauthenticated (conn);
	    reply (conn, "530 Failed to authenticate: %s (root dir inaccessible?).", strerror (errno));
	    break;
	  }
	  user_setup_environ (conn->user, 1, conn->extRFd, conn->extWFd);
	  
	  if (conn->user->anonymous)
	    reply (conn, "230 Anonymous access granted.");
	  else
	    reply (conn, "230 Logged in as %s.", conn->user->name);
	}
	else
	{
	  unauthenticated (conn);
	  
	  if (!strcmp (bp, "DENY"))
	    reply (conn, "530 Login limit reached.");
	  else
	    reply (conn, "530 Accounter error. Please try again later.");
	}
	break;
      case acSending:
	if (!strcmp (bp, "ALLOW"))
	{
	  if (open_data_connection (conn))
	  {
	    reply (conn, "425 Failed to open data connection: %s.", strerror(errno));
	    pfree (conn->filePath, conn);
	    conn->filePath = NULL;
	    if (close_data_connection (conn))
	      return 0;
	  }
	  else
	  {
	    user_setup_environ (conn->user, conn->authed, conn->extRFd, conn->extWFd);
	    
	    if (conn->type == ttImage)
	      reply (conn, "150 Opening BINARY data connection for sending of \"%s\" (%lld bytes).",
		    print_path (conn->filePath), conn->dataLen);
	    else
	      reply (conn, "150 Opening ASCII data connection for sending of \"%s\".",
		    print_path (conn->filePath));
	    conn->working = 1;
	  }
	}
	else
	{
	  if (!strcmp (bp, "DENY"))
	    reply (conn, "550 Permission denied.");
	  else
	    reply (conn, "550 Accounter error. Please try again later.");
	  pfree (conn->filePath, conn);
	  conn->filePath = NULL;
	  if (close_data_connection (conn))
	    return 0;
	}
	break;
      case acGetting:
	if (!strcmp (bp, "ALLOW"))
	{
	  if (open_data_connection (conn))
	  {
	    reply (conn, "425 Failed to open data connection: %s.", strerror (errno));
	    pfree (conn->filePath, conn);
	    conn->filePath = NULL;
	    if(close_data_connection (conn) == -1)
	      return 0;
	  }
	  else
	  {
	    user_setup_environ (conn->user, conn->authed, conn->extRFd, conn->extWFd);
	    
	    reply (conn, "150 Opening %s data connection for retrieval of \"%s\".",
		  conn->type == ttImage? "BINARY" : "ASCII", print_path (conn->filePath));
	    conn->working = 1;
	  }
	}
	else
	{
	  if (!strcmp (bp, "DENY\n"))
	    reply (conn, "550 Permission denied.");
	  else
	    reply (conn, "550 Accounter error. Please try again later.");
	  pfree (conn->filePath, conn);
	  conn->filePath = NULL;
	  if (close_data_connection (conn))
	    return 0;
	}
	break;
      case acList:
	if (!strcmp (bp, "END"))
	  reply (conn, "200 End of list.");
	else
	{
	  bp[strlen (bp) - 1] = 0;
	  reply (conn, " %s", bp);
	  continue;
	}
	break;
      case acMsg:
	if (!strcmp (bp, "OK"))
	  reply (conn, "200 Message sent.");
	else if (!strcmp (bp, "INVALID"))
	  reply (conn, "501 Invalid connection id.");
	else
	  reply (conn, "500 Unknown error.");
	break;
      case acAbort:
	if (!strcmp (bp, "OK"))
	  reply (conn, "200 Transfer aborted.");
	else if (!strcmp (bp, "INVALID"))
	  reply (conn, "501 Invalid connection id.");
	else
	  reply (conn, "500 Unknown error.");
	break;
      case acDisconnect:
	if (!strcmp (bp, "OK"))
	  reply (conn, "200 Disconnected.");
	else if (!strcmp (bp, "INVALID"))
	  reply (conn, "501 Invalid connection id.");
	else
	  reply (conn, "500 Unknown error.");
	break;
      }
      conn->accWait = 0;
    }
  }
  if (!conn->accWait && !conn->sleepUntil)
    run_commands (conn);
  return 0;
}

ssize_t conn_plain_writer (const void *channel, const void *buf, size_t len)
{
  return write ((int)channel, buf, len);
}

static ssize_t conn_plain_vecs_writer (const void *channel, struct iovec *vecs, int num)
{
  struct msghdr msg = {0};
  
  msg.msg_iov = vecs;
  msg.msg_iovlen = num;
  return sendmsg ((int)channel, &msg, 0);
}

int count_connections (void)
{
  connection_t *conn;
  int i = 0;
  
  for (conn = pchild (connections, NULL); conn; conn = pchild (connections, conn))
    i++;
  
  return i;
}

connection_t *new_connection(int sock, server_t *serv, int accSock)
{
  connection_t *res;
  struct sockaddr_storage lAddr, rAddr;
  int lAl, rAl;
  char rhost[NI_MAXHOST], lhost[NI_MAXHOST], lport[NI_MAXSERV];
  int i;
  
  /* Check that it really is a socket and get the local address/port. */
  lAl = sizeof(lAddr);
  if(getsockname(sock, (struct sockaddr*)&lAddr, &lAl))
    return NULL;
  rAl = sizeof(lAddr);
  if(getpeername(sock, (struct sockaddr*)&rAddr, &rAl))
    return NULL;
  
  if (getnameinfo ((struct sockaddr*)&rAddr, rAl, rhost, sizeof (rhost), NULL,
	    0, NI_NUMERICHOST))
    strcpy (rhost, "unknown");
  
  if(!connections)
  {
    connections = proot();
    if(!connections)
      return NULL;
  }
  
  res = palloc(sizeof(connection_t), connections, NULL);
  if(!res)
    return NULL;
  res->sock = sock;
  res->langFd = -1;
  res->id = -1;
  res->controlWrite = conn_plain_writer;
  res->controlWriteVecs = conn_plain_vecs_writer;
  res->controlChannel = (void*)sock;
  
  if(serv)
    res->server = pattach(serv, res);
  else
  {
    /* Find the server running on this address/port. */
    res->server = pattach(find_server((struct sockaddr*)&lAddr, (struct sockaddr*)&rAddr, NULL), res);
    if(!res->server)
    {
      if (getnameinfo ((struct sockaddr*)&lAddr, lAl, lhost, sizeof (lhost), lport,
		sizeof (lport), NI_NUMERICHOST | NI_NUMERICSERV))
      {
	strcpy (lhost, "unknown");
	strcpy (lport, "unknown");
      }
      syslog (LOG_NOTICE, "Connection from %s, but no server on %s port %s", rhost, lhost, lport);
      reply(res, "421 No service available at this address.");
      close (res->sock);
      pfree(res, connections);
      if(!errno)
	errno = EINVAL;
      return NULL;
    }
  }
  if (accSock >= 0)
    res->accSock = accSock;
  else
    res->accSock = connect_accounter ();
  if (res->accSock < 0)
  {
    reply (res, "421 Accounter is not available.");
    close (res->sock);
    pfree (res, connections);
    if (!errno)
      errno = EINVAL;
    return NULL;
  }
  accounter (res->accSock, "SET PID %d\n", (int)getpid ());
  
  switch(lAddr.ss_family)
  {
  case AF_INET:
    res->activePort = ntohs (((struct sockaddr_in*)&lAddr)->sin_port) - 1;
    ((struct sockaddr_in*)&lAddr)->sin_port = htons (res->activePort);
    break;
  case AF_INET6:
    res->activePort = ntohs (((struct sockaddr_in6*)&lAddr)->sin6_port) - 1;
    ((struct sockaddr_in6*)&lAddr)->sin6_port = htons (res->activePort);
    break;
  }
  if(!res->server->allowLowPorts)
  {
    switch(rAddr.ss_family)
    {
    case AF_INET:
      if(ntohs (((struct sockaddr_in*)&rAddr)->sin_port) < 1024)
      {
	syslog (LOG_NOTICE, "%d: Client port < 1024", res->sock);
	reply(res, "421 Server does not accept client ports < 1024.");
	close (res->sock);
	pfree(res, connections);
	errno = EINVAL;
	return NULL;
      }
      break;
    case AF_INET6:
      if(ntohs (((struct sockaddr_in6*)&rAddr)->sin6_port) < 1024)
      {
	syslog (LOG_NOTICE, "%d: Client port < 1024", res->sock);
	reply(res, "421 Server does not accept client ports < 1024.");
	close (res->sock);
	pfree(res, connections);
	errno = EINVAL;
	return NULL;
      }
    }
  }
  res->lDataAddr = lAddr;
  res->rDataAddr = rAddr;
  res->dataSock = -1;
  res->fileFd = -1;
  res->mlstTags = tfType | tfUnique | tfModify | tfPerm | tfSize;
  res->extRFd = res->extWFd = -1;
  
  i = -1;
  setsockopt (sock, SOL_SOCKET, SO_OOBINLINE, &i, sizeof (i));
  i = IPTOS_LOWDELAY;
  setsockopt (sock, IPPROTO_IP, IP_TOS, &i, sizeof (i));
  
  if (accounter (res->accSock, "CONNECT %s %s\n", rhost, res->server->name))
  {
    syslog (LOG_ERR, "%d: Error in acocunter: %m", res->sock);
    reply (res, "421 Error: %s. Please try again later.", strerror (errno));
    disconnected (res);
    return NULL;
  }
  res->accWait = acWelcome;
  
  /* Only add accounter fd for now. */
  add_read_fd (res->accSock, accounter_reply, res);
  
  return res;
}

void disconnected(connection_t *conn)
{
  unauthenticated (conn);
  
  conn->spontQuit = 0;
  close_data_connection (conn);
  remove_read_fd(conn->sock);
  remove_read_fd (conn->accSock);
  close (conn->accSock);
  conn->accSock = -1;
  syslog (LOG_INFO, "%d: Disconnected", conn->id);
#ifdef USE_TLS
  if (conn->tlsControl)
  {
    tls_stop (conn->tlsControl);
    tls_free (conn->tlsControl);
  }
  if (conn->tlsData)
  {
    tls_stop (conn->tlsData);
    tls_free (conn->tlsData);
  }
  if (conn->tlsClientCert)
    tls_free_cert (conn->tlsClientCert);
#endif
  close(conn->sock);
  conn->sock = -1;
  if (conn->langFd >= 0)
    close_shared (conn->langFd);
#ifdef HAVE_LIBPAM
  if (conn->pamh)
    pam_end (conn->pamh, PAM_SUCCESS);
  conn->pamh = NULL;
#endif
  if (conn->inLookUp)
  {
#ifdef USE_SQL
    if (!--conn->server->sqlRefs)
      sql_disconnect (&conn->server->sql);
#endif
  }
  pfree(conn, connections);
}

void quit_all_connections(const char *str)
{
  connection_t *conn;
  
  if (!connections)
    return;
  
  for(conn = pchild(connections, NULL); conn; conn = pchild(connections, conn))
    reply_spont(conn, str, 1);
}

void disconnect_all_connections(void)
{
  connection_t *conn;
  
  if (!connections)
    return;
  
  while((conn = pchild(connections, NULL)))
    disconnected(conn);
}

void check_idle(void)
{
  connection_t *conn, *nconn;
  time_t currtime;
  int maxIdle;
  
  if(!connections)
    return;
  
  time(&currtime);
  
  for(conn = pchild(connections, NULL); conn; conn = nconn)
  {
    nconn = pchild(connections, conn);
    
    if (conn->accWait)
      continue;
    
    if (conn->sleepUntil && currtime >= conn->sleepUntil)
    {
      conn->sleepUntil = 0;
      run_commands (conn);
    }
    
    if(conn->user)
      maxIdle = conn->user->maxIdle;
    else
      maxIdle = conn->server->maxIdle;
    
    if(maxIdle && !conn->working)
    {
      if(currtime > conn->lastCommand + maxIdle * 2)
	disconnected(conn);
      else if(currtime > conn->lastCommand + maxIdle && !conn->spontQuit)
	reply_spont(conn, "421 Connection timed out.", 1);
    }
  }
}

void reply(connection_t *conn, const char *format, ...)
{
  va_list ap;
  char *str;
  int doFree = 0, isLastLine = 1;
  
  if (conn->langFd >= 0 && format)
    format = scan_strings (conn->langFd, format);
  
  if (format)
  {
    va_start (ap, format);
#ifdef HAVE_VASPRINTF
    vasprintf (&str, format, ap);
    doFree = 1;
#else
    str = talloc (4097);
    if (str)
      vsnprintf (str, 4096, format, ap);
#endif
    va_end (ap);
    
    if (!str)
    {
      str = tstring (format);
      doFree = 0;
    }
    
    if (str)
      isLastLine = (str[0] != ' ' && str[3] == ' ');
    
    // Change the numeric code if needed.
    if (str && conn->spontReply && str[0] != ' ')
    {
      str[3] = '-';
      if (conn->spontQuit)
	strncpy (str, conn->spontReply, 3);
      else
	strncpy (conn->spontReply, str, 3);
    }
    send_telnet(conn, str, 1);
  }
  
  // Only send spontaneous reply after last line of a previous message.
  if (conn->spontReply && (!format || isLastLine))
  {
    send_telnet(conn, conn->spontReply, 1);
    pfree(conn->spontReply, conn);
    conn->spontReply = NULL;
  }
  
  time(&conn->lastCommand);
  
#if HAVE_VASPRINTF
  if(doFree)
    free(str);
#endif
}

int reply_file (connection_t *conn, const char *format, const char *file, const char *lastfmt)
{
  int fd = open (file, O_RDONLY);
  char buf[1025], *nbp;
  int l, bl;
  
  if (fd < 0)
  {
    syslog (LOG_ERR, "Failed to open %s: %m.", file);
    return -1;
  }
  
  bl = 0;
  while ((l = read (fd, buf + bl, 1024 - bl)) > 0 || bl > 0)
  {
    if (l > 0)
      bl += l;
    buf[bl] = 0;
    nbp = strchr (buf, '\n');
    if (!nbp)
      break;
    *nbp++ = 0;
    if (bl < 1024 && !*nbp) // Check for file ending with newline.
      break;
    
    reply (conn, format, buf);
    if (!nbp)
      break;
    
    bl -= nbp - buf;
    memmove (buf, nbp, bl);
  }
  
  if (bl)
  {
    if (lastfmt)
      format = lastfmt;
    reply (conn, format, buf);
  }
  
  close (fd);
  return 0;
}

void reply_msg (connection_t *conn, const char *format, const char *msg)
{
  if (msg[0] == '/')
  {
    char *fmt = tstring (format);
    
    fmt[3] = '-';
    reply_file (conn, fmt, msg, format);
  }
  else
    reply (conn, format, msg);
}

void reply_spont(connection_t *conn, const char *str, int closeConn)
{
  pfree (conn->spontReply, conn);
  if (conn->langFd >= 0)
    conn->spontReply = pstring (scan_strings (conn->langFd, str), conn);
  else
    conn->spontReply = pstring (str, conn);
  if (closeConn)
    conn->spontQuit = 1;
}

#define CHECK_VEC_SSP \
      if (cvec >= svec - 1) \
      { \
	svec *= 2; \
	vecs = trealloc (vecs, sizeof (struct iovec) * svec); \
	if (!vecs) \
	  return; \
      } \
      if (ssp < sp) \
      { \
	vecs[cvec].iov_base = (void*)ssp; \
	vecs[cvec++].iov_len = sp - ssp; \
      }

void send_telnet(connection_t *conn, const char *str, int newline)
{
  const char *sp, *ssp;
  struct iovec *vecs = talloc (sizeof (struct iovec) * 4);
  int cvec = 0, svec = 4;
  
  syslog (LOG_DEBUG, "%d: SEND: %s", conn->id, str);
  
  for (ssp = sp = str; *sp; sp++)
  {
    switch (*(unsigned char*)sp)
    {
    case '\r':
      if (*++sp != '\n')
      {
	if (vecs)
	{
	  CHECK_VEC_SSP;
	  vecs[cvec].iov_base = (void*)"";
	  vecs[cvec++].iov_len = 1;
	}
	else
	  conn->controlWrite (conn->controlChannel, ssp, sp - ssp);
	ssp = sp--;
      }
      break;
    case 255:
      if (vecs)
      {
	CHECK_VEC_SSP;
	vecs[cvec].iov_base = (void*)"\xFF";
	vecs[cvec++].iov_len = 1;
      }
      else
      {
	if (ssp < sp)
	  conn->controlWrite (conn->controlChannel, ssp, sp - ssp);
	conn->controlWrite (conn->controlChannel, "\xFF", 1);
      }
      ssp = sp;
      break;
    default:
      break;
    }
  }
  if (vecs)
  {
    CHECK_VEC_SSP;
    if (newline)
    {
      vecs[cvec].iov_base = (void*)"\r\n";
      vecs[cvec++].iov_len = 2;
    }
    conn->controlWriteVecs (conn->controlChannel, vecs, cvec);
  }
  else
  {
    if (ssp < sp)
      conn->controlWrite (conn->controlChannel, ssp, sp - ssp);
    if (newline)
      conn->controlWrite (conn->controlChannel, "\r\n", 2);
  }
}

int control_handler(int sock, void *user, int urgent)
{
  connection_t *conn = user;
  int l;
  char *buffp;
  
  if (conn->sock == -1)
    return 0;
  
  if(urgent)
  {
    char *tmpBuf = talloc(1024);
    
    /*
     * Discard all urgent data after telnet parsing.
     */
    if(tmpBuf)
    {
      l = recv(sock, tmpBuf, 1024, MSG_OOB);
      if(l > 0)
	parse_telnet(conn, tmpBuf, l);
    }
    return 0;
  }
  
  if(conn->readBufferLen >= sizeof(conn->readBuffer) - 1)
  {
    /* Someone sent us a line longer than we can handle. Shame on them! */
    syslog (LOG_NOTICE, "%d: Input line overflow.", conn->id);
    conn->overFlow = 1;
    conn->readBufferLen = 0;
  }
  
  /* Append what we read. */
  buffp = conn->readBuffer + conn->readBufferLen;
#ifdef USE_TLS
  if (conn->tlsControl)
  {
    l = -1;
    switch (conn->tlsState)
    {
    case 0:
      l = tls_accept (conn->tlsControl);
      if (l == 1)
      {
	syslog (LOG_INFO, "%d: Successfully negotiated TLS.", conn->id);
	conn->tlsState = 1;
	conn->controlWrite = (writeFun_t)tls_write;
	conn->controlWriteVecs = (writeVecsFun_t)tls_write_vecs;
	conn->controlChannel = conn->tlsControl;
	if (conn->tlsClientCert)
	  tls_free_cert (conn->tlsClientCert);
	conn->tlsClientCert = tls_get_peer_cert (conn->tlsControl);
	if ((conn->server->tlsOptions & tlsVerifyClient) && conn->server->loginTLS)
	{
	  const char *clientName = tstring (tls_get_cn (conn->tlsClientCert));
	  
	  if (clientName)
	  {
	    if (!conn->inLookUp)
	    {
	      conn->inLookUp = 1;
#ifdef USE_SQL
	      conn->server->sqlRefs++;
#endif
	    }
	    conn->user = find_user (conn, clientName, 1, conn);
	    if(conn->user)
	    {
	      if (conn->user->allowSecLogin)
	      {
#ifdef HAVE_LIBPAM
		pam_set_item (conn->pamh, PAM_USER, conn->user->name);
#endif
		if (authenticated (conn))
		{
		  reply (conn, "421 Failed to authenticate: %s (root dir inaccessible?).", strerror (errno));
		  disconnected (conn);
		  return 0;
		}
		user_setup_environ (conn->user, 1, conn->extRFd, conn->extWFd);
		return 0;
	      }
	      pfree (conn->user, conn);
	      conn->user = NULL;
	    }
	    syslog (LOG_NOTICE, "%d: No user for CN \"%s\".", conn->id, clientName);
	  }
	  else
	    syslog (LOG_NOTICE, "%d: No CN in certificate.", conn->id);
	  if (conn->server->tlsNoLogout)
	  {
	    reply (conn, "421 Failed to find a user for your certificate.");
	    disconnected (conn);
	  }
	}
      }
      else if (l)
      {
	if (l == -1)
	  reply (conn, "421 TLS negotiation failed.");
	syslog (LOG_NOTICE, "%d: Error in TLS negotiation: %s.", conn->id, tls_error (conn->tlsControl, l));
	disconnected (conn);
      }
      return 0;
    case 1:
      l = tls_read (conn->tlsControl, buffp, sizeof (conn->readBuffer) -
	    conn->readBufferLen);
      break;
    case 2:
      l = tls_stop (conn->tlsControl);
      if (l == 1)
      {
	tls_free (conn->tlsControl);
	conn->tlsControl = NULL;
      }
      else if (l)
      {
	syslog (LOG_NOTICE, "%d: Error in TLS shutdown: %s.", conn->id,
	      tls_error (conn->tlsControl, l));
	disconnected (conn);
      }
      return 0;
    }
  }
  else
#endif
    l = read (sock, buffp, sizeof (conn->readBuffer) - conn->readBufferLen);
  if(l < 0)
  {
    /* Error */
#ifdef USE_TLS
    if (conn->tlsControl)
    {
      syslog (LOG_DEBUG, "%d: TLS read error: %s.", conn->id,
	    tls_error (conn->tlsControl, l));
      disconnected (conn);
    }
    else
#endif
      if(errno != EINTR)
      {
	syslog (LOG_DEBUG, "%d: control_handler: read: %m", conn->id);
	disconnected (conn);
      }
    return 0;
  }
  if(!l)
  {
    syslog (LOG_DEBUG, "%d: Client suddenly disconnected.", conn->id);
    /* EOF == they disconnected. */
    disconnected(conn);
    return 0;
  }
  
  /* Parse telnet stuff. */
  conn->readBufferLen += parse_telnet(conn, buffp, l);
  
  if (!conn->sleepUntil) // Will be run by check_idle otherwise.
    run_commands (conn);
  return 0;
}

void run_commands (connection_t *conn)
{
  char *buffp;
  
  /* Check for line breaks. */
  while((buffp = memchr(conn->readBuffer, 0, conn->readBufferLen)))
  {
    if(conn->overFlow)
    {
      /* We can't handle this. Beginning of line is already discarded. */
      conn->overFlow = 0;
      reply(conn, "500 Line too long.");
    }
    else
    {
      if (debug)
      {
	if (!strncasecmp (conn->readBuffer, "PASS ", 5))
	  syslog (LOG_DEBUG, "%d: RECV: PASS (hidden)", conn->id);
	else
	  syslog (LOG_DEBUG, "%d: RECV: %s", conn->id, conn->readBuffer);
      }
      if(handle_command(conn, conn->readBuffer))
	return;
    }
    
    /* Discard line */
    conn->readBufferLen -= ++buffp - conn->readBuffer;
    memmove(conn->readBuffer, buffp, conn->readBufferLen);
  }
}

int parse_telnet(connection_t *conn, char *buff, int len)
{
  unsigned char *curr, *end = (unsigned char*)buff + len;
  unsigned char retBuf[3];
  
  /*
   * Telnet command handling. Answer all DO with WON'T, and all WILL with DON'T
   * for now. Remove all null characters and put nulls at linebreaks.
   */
  for(curr = (unsigned char*)buff; curr < end;)
  {
    switch(conn->telnetMode)
    {
    case NUL:
      switch(*curr)
      {
      case NUL:
	if(conn->telnetFlags & tfCR)
	{
	  *curr = '\r';
	  if(conn->telnetFlags & tfSubOpt)
	  {
#if 0
	    if(conn->suboptLen < 100)
	      conn->subopt[conn->suboptLen++] = *curr;
#endif
	    memmove(curr, curr + 1, --end - curr);
	  }
	  else
	    curr++;
	}
	else
	  memmove(curr, curr + 1, --end - curr);
	conn->telnetFlags &= ~tfCR;
	break;
      case '\n':
	if(conn->telnetFlags & tfCR)
	  *curr = 0;
	conn->telnetFlags &= ~tfCR;
	if(conn->telnetFlags & tfSubOpt)
	{
#if 0
	  if(conn->suboptLen < 100)
	    conn->subopt[conn->suboptLen++] = *curr;
#endif
	  memmove(curr, curr + 1, --end - curr);
	}
	else
	  curr++;
	break;
      case '\r':
	conn->telnetFlags |= tfCR;
	memmove(curr, curr + 1, --end - curr);
	/*
	 * CR will be ignored unless followed by either NL or NUL, but that
	 * what the telnet rfc says a CR must be followed by so.
	 */
	break;
      case IAC:
	conn->telnetFlags &= ~tfCR;
	conn->telnetMode = IAC;
	memmove(curr, curr + 1, --end - curr);
	break;
      default:
	conn->telnetFlags &= ~tfCR;
	if (conn->telnetFlags & tfSubOpt)
	{
#if 0
	  if (conn->suboptLen < 100)
	    conn->subopt[conn->suboptLen++] = *curr;
#endif
	  memmove (curr, curr + 1, --end - curr);
	}
	else
	  curr++;
	break;
      }
      break;
    case IAC:
      switch(*curr)
      {
      case IAC:
	// Quoted character 255
	if(conn->telnetFlags & tfSubOpt)
	{
#if 0
	  if(conn->suboptLen < 100)
	    conn->subopt[conn->suboptLen++] = *curr;
#endif
	  memmove(curr, curr + 1, --end - curr);
	}
	else
	  curr++;
	conn->telnetMode = NUL;
	break;
      case SB:
	memmove(curr, curr + 1, --end - curr);
	conn->telnetMode = SB;
	break;
      case SE:
	conn->telnetMode = SE;
	memmove(curr, curr + 1, --end - curr);
	break;
      case WILL:
      case WONT:
      case DO:
      case DONT:
	conn->telnetMode = *curr;
	memmove(curr, curr + 1, --end - curr);
	break;
      default:
	if(*curr >= 240)
	  memmove(curr, curr + 1, --end - curr);
	else
	  curr++;
	conn->telnetMode = NUL;
	break;
      }
      break;
    case SB:
      syslog (LOG_NOTICE, "%d: Unnegotiated suboption.", conn->id);
      conn->telnetMode = NUL;
      if (conn->telnetFlags & tfSubOpt)
	syslog (LOG_NOTICE, "%d: Nested suboption.", conn->id);
      conn->telnetFlags |= tfSubOpt;
      conn->sb = *curr;
      memmove (curr, curr + 1, --end - curr);
      break;
    case SE:
      conn->telnetMode = NUL;
      if (!(conn->telnetFlags & tfSubOpt))
	syslog (LOG_NOTICE, "%d: Suboption end without suboption start.",
	      conn->id);
      else if(*curr == conn->sb)
      {
	conn->telnetFlags &= ~tfSubOpt;
#if 0
	conn->suboptLen = 0;
#endif
      }
      else
	syslog (LOG_NOTICE, "%d: Wrong suboption ended, got %d expected %d.",
	      conn->id, *curr, conn->sb);
      memmove (curr, curr + 1, --end - curr);
      break;
    case WILL:
    case DO:
      retBuf[0] = IAC;
      retBuf[1] = (conn->telnetMode == WILL? DONT : WONT);
      retBuf[2] = *curr;
      write(conn->sock, retBuf, 3);
      conn->telnetMode = NUL;
      memmove (curr, curr + 1, --end - curr);
      break;
    case WONT:
    case DONT:
      conn->telnetMode = NUL;
      memmove (curr, curr + 1, --end - curr);
      break;
    }
  }
  return (char*)end - buff;
}

int authenticated(connection_t *conn)
{
  conn->authed = 1;
  if (conn->inLookUp)
  {
    conn->inLookUp = 0;
#ifdef USE_SQL
    if (!--conn->server->sqlRefs)
      sql_disconnect (&conn->server->sql);
#endif
  }
  if (conn->extRFd >= 0)
  {
    remove_read_fd (conn->extRFd);
    close (conn->extRFd);
    conn->extRFd = -1;
  }
  if (conn->extWFd >= 0)
  {
    close (conn->extWFd);
    conn->extWFd = -1;
  }
  if(conn->user)
  {
    if (conn->email)
      syslog (LOG_INFO, "%d: User %s (email \"%s\") logged in.", conn->id,
	    conn->user->name, conn->email);
    else if (conn->account)
      syslog (LOG_INFO, "%d: User %s (account \"%s\") logged in.", conn->id,
	    conn->user->name, conn->account);
    else
      syslog (LOG_INFO, "%d: User %s logged in.", conn->id, conn->user->name);
    if (conn->user->access_program)
    {
      int ipipe[2], opipe[2], i;
      const char *args[4], **ap = args;
      const char *envs[] = { NULL };
      const char *prog = conn->user->access_program;
      
      conn->mlstTags &= ~tfPerm; // perm might be heavy if checked externally.
      
      if (pipe (ipipe) || pipe (opipe))
	conn->extRFd = conn->extWFd = -1;
      else
      {
	*ap++ = prog;
	*ap++ = conn->user->name;
	if (conn->account)
	  *ap++ = conn->account;
	*ap = NULL;
	switch (vfork ())
	{
	case -1:
	  // Error
	  close (ipipe[0]);
	  close (ipipe[1]);
	  close (opipe[0]);
	  close (opipe[1]);
	  conn->extRFd = conn->extWFd = -1;
	  break;
	case 0:
	  // Child
	  if (dup2 (ipipe[1], 1) == -1)
	    _exit (1);
	  if (dup2 (opipe[0], 0) == -1)
	    _exit (1);
	  for (i = 2; i < getdtablesize (); i++)
	    close (i);
	  execve (prog, (char**)args, (char**)envs);
	  _exit (1);
	default:
	  close (ipipe[1]);
	  close (opipe[0]);
	  conn->extRFd = ipipe[0];
	  conn->extWFd = opipe[1];
	  break;
	}
      }
    }
    if(conn->user->chroot)
    {
      const char *new_root;
      
      super_privs (0);
      new_root = set_root (conn->user->chroot);
      
      if (!new_root)
      {
	syslog (LOG_ERR, "%d: Failed to chroot: %m.", conn->id);
	return -1;
      }
      if (strcmp (new_root, conn->user->chroot))
      {
	pfree (conn->user->chroot, conn->user);
	conn->user->chroot = pstring (new_root, conn->user);
      }
    }
    if(conn->user->home)
      conn->cwd = pstring (set_cwd ("~", conn->user->home), conn);
  }
  else
    syslog (LOG_NOTICE, "%d: Unknown user logged in.", conn->id);
#ifdef HAVE_LIBPAM
  super_privs (0);
  // TODO: Check return values.
  pam_setcred (conn->pamh, PAM_ESTABLISH_CRED);
  pam_open_session (conn->pamh, PAM_SILENT);
#endif
  return 0;
}

void unauthenticated(connection_t *conn)
{
#ifdef HAVE_LIBPAM
  super_privs (0);
  // TODO: Check return values.
  if (conn->pamh)
  {
    pam_close_session (conn->pamh, PAM_SILENT);
    pam_setcred (conn->pamh, PAM_DELETE_CRED);
  }
#endif
  conn->authed = 0;
  pfree(conn->user, conn);
  conn->user = NULL;
  pfree(conn->email, conn);
  conn->email = NULL;
  pfree (conn->account, conn);
  conn->account = NULL;
  if (conn->extRFd >= 0)
  {
    remove_read_fd (conn->extRFd);
    close (conn->extRFd);
    conn->extRFd = -1;
  }
  if (conn->extWFd >= 0)
  {
    close (conn->extWFd);
    conn->extWFd = -1;
  }
}


syntax highlighted by Code2HTML, v. 0.9.1