/*
 * ProFTPD - FTP server daemon
 * Copyright (c) 2001-2020 The ProFTPD Project team
 *
 * 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., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA.
 *
 * As a special exemption, the ProFTPD Project team and other respective
 * copyright holders give permission to link this program with OpenSSL, and
 * distribute the resulting executable, without including the source code for
 * OpenSSL in the source distribution.
 */

/* NetIO routines */

#include "conf.h"

/* See RFC 854 for the definition of these Telnet values */

/* Telnet "Interpret As Command" indicator */
#ifndef TELNET_IAC
# define TELNET_IAC	255
#endif

#ifndef TELNET_DONT
# define TELNET_DONT	254
#endif

#ifndef TELNET_DO
# define TELNET_DO	253
#endif

#ifndef TELNET_WONT
# define TELNET_WONT	252
#endif

#ifndef TELNET_WILL
# define TELNET_WILL	251
#endif

/* Telnet "Interrupt Process" code */
#ifndef TELNET_IP
# define TELNET_IP	244
#endif

/* Telnet "Data Mark" code */
#ifndef TELNET_DM
# define TELNET_DM	242
#endif

static const char *trace_channel = "netio";

static pr_netio_t *default_ctrl_netio = NULL, *ctrl_netio = NULL;
static pr_netio_t *default_data_netio = NULL, *data_netio = NULL;
static pr_netio_t *default_othr_netio = NULL, *othr_netio = NULL;

/* Used to track whether the previous text read from the client's control
 * connection was a properly-terminated command.  If so, then read in the
 * next/current text as per normal.  If NOT (e.g. the client sent a too-long
 * command), then read in the next/current text, but ignore it.  Only clear
 * this flag if the next/current command can be read as per normal.
 *
 * The pr_netio_telnet_gets() uses this variable, in conjunction with its
 * saw_newline flag, for handling too-long commands from clients.
 */
static int properly_terminated_prev_command = TRUE;

static pr_netio_stream_t *netio_stream_alloc(pool *parent_pool) {
  pool *netio_pool = NULL;
  pr_netio_stream_t *nstrm = NULL;

  if (!parent_pool) {
    errno = EINVAL;
    return NULL;
  }

  netio_pool = make_sub_pool(parent_pool);
  pr_pool_tag(netio_pool, "netio stream pool");
  nstrm = pcalloc(netio_pool, sizeof(pr_netio_stream_t));

  nstrm->strm_pool = netio_pool;
  nstrm->strm_fd = -1;
  nstrm->strm_mode = 0;
  nstrm->strm_flags = 0;
  nstrm->strm_buf = NULL;
  nstrm->strm_data = NULL;
  nstrm->strm_errno = 0;

  /* This table will not contain that many entries, so a low number
   * of chains should suffice.
   */
  nstrm->notes = pr_table_nalloc(nstrm->strm_pool, 0, 4);

  return nstrm;
}

pr_buffer_t *pr_netio_buffer_alloc(pr_netio_stream_t *nstrm) {
  size_t bufsz;
  pr_buffer_t *pbuf = NULL;

  if (nstrm == NULL) {
    errno = EINVAL;
    return NULL;
  }

  pbuf = pcalloc(nstrm->strm_pool, sizeof(pr_buffer_t));

  /* Allocate a buffer. */
  bufsz = pr_config_get_server_xfer_bufsz(nstrm->strm_mode);
  pbuf->buf = pcalloc(nstrm->strm_pool, bufsz);
  pbuf->buflen = bufsz;

  /* Position the offset at the start of the buffer, and set the
   * remaining bytes value accordingly.
   */
  pbuf->current = pbuf->buf;
  pbuf->remaining = bufsz;

  /* Add this buffer to the given stream. */
  nstrm->strm_buf = pbuf;

  return pbuf;
}

/* Default core NetIO handlers
 */

static void core_netio_abort_cb(pr_netio_stream_t *nstrm) {
  nstrm->strm_flags |= PR_NETIO_SESS_ABORT;
}

static int core_netio_close_cb(pr_netio_stream_t *nstrm) {
  int res = 0;

  if (nstrm->strm_fd != -1) {
    res = close(nstrm->strm_fd);
    nstrm->strm_fd = -1;

  } else {
    errno = EBADF;
    res = -1;
  }

  return res;
}

static pr_netio_stream_t *core_netio_open_cb(pr_netio_stream_t *nstrm, int fd,
    int mode) {

  nstrm->strm_fd = fd;

  /* The stream's strm_mode field does not need to be set, as it is set
   * by the NetIO layer's open() wrapper function.
   */

  return nstrm;
}

static int core_netio_poll_cb(pr_netio_stream_t *nstrm) {
  int res;
  fd_set rfds, *rfdsp, wfds, *wfdsp;
  struct timeval tval;

  FD_ZERO(&rfds);
  rfdsp = NULL;
  FD_ZERO(&wfds);
  wfdsp = NULL;

  if (nstrm->strm_mode == PR_NETIO_IO_RD) {
    if (nstrm->strm_fd >= 0) {
      FD_SET(nstrm->strm_fd, &rfds);
      rfdsp = &rfds;
    }

  } else {
    if (nstrm->strm_fd >= 0) {
      FD_SET(nstrm->strm_fd, &wfds);
      wfdsp = &wfds;
    }
  }

  tval.tv_sec = ((nstrm->strm_flags & PR_NETIO_SESS_INTR) ?
    nstrm->strm_interval: 60);
  tval.tv_usec = 0;

  res = select(nstrm->strm_fd + 1, rfdsp, wfdsp, NULL, &tval);
  while (res < 0) {
    int xerrno = errno;

    if (!(nstrm->strm_flags & PR_NETIO_SESS_INTR)) {
      /* Watch for EAGAIN, and handle it by delaying temporarily. */
      if (xerrno == EAGAIN) {
        errno = EINTR;
        pr_signals_handle();
        continue;
      }
    }

    errno = nstrm->strm_errno = xerrno;
    break; 
  }

  return res;
}

static int core_netio_postopen_cb(pr_netio_stream_t *nstrm) {
  return 0;
}

static int core_netio_read_cb(pr_netio_stream_t *nstrm, char *buf,
    size_t buflen) {
  return read(nstrm->strm_fd, buf, buflen);
}

static pr_netio_stream_t *core_netio_reopen_cb(pr_netio_stream_t *nstrm, int fd,
    int mode) {

  if (nstrm->strm_fd != -1) {
    close(nstrm->strm_fd);
  }

  nstrm->strm_fd = fd;
  nstrm->strm_mode = mode;

  return nstrm;
}

static int core_netio_shutdown_cb(pr_netio_stream_t *nstrm, int how) {
  return shutdown(nstrm->strm_fd, how);
}

static int core_netio_write_cb(pr_netio_stream_t *nstrm, char *buf,
    size_t buflen) {
  return write(nstrm->strm_fd, buf, buflen);
}

static const char *netio_stream_mode(int strm_mode) {
  const char *modestr = "(unknown)";

  switch (strm_mode) {
    case PR_NETIO_IO_RD:
      modestr = "reading";
      break;

    case PR_NETIO_IO_WR:
      modestr = "writing";
      break;

    default:
      break;
  }

  return modestr;
}

/* NetIO API wrapper functions. */

void pr_netio_abort(pr_netio_stream_t *nstrm) {
  const char *nstrm_mode;

  if (nstrm == NULL) {
    errno = EINVAL;
    return;
  }

  nstrm_mode = netio_stream_mode(nstrm->strm_mode);

  switch (nstrm->strm_type) {
    case PR_NETIO_STRM_CTRL:
      if (ctrl_netio != NULL) {
        pr_trace_msg(trace_channel, 19,
          "using %s abort() for control %s stream",
          ctrl_netio->owner_name, nstrm_mode);
        (ctrl_netio->abort)(nstrm);

      } else {
        pr_trace_msg(trace_channel, 19,
          "using %s abort() for control %s stream",
          default_ctrl_netio->owner_name, nstrm_mode);
        (default_ctrl_netio->abort)(nstrm);
      }

      break;

    case PR_NETIO_STRM_DATA:
      if (data_netio != NULL) {
        pr_trace_msg(trace_channel, 19, "using %s abort() for data %s stream",
          data_netio->owner_name, nstrm_mode);
        (data_netio->abort)(nstrm);

      } else {
        pr_trace_msg(trace_channel, 19, "using %s abort() for data %s stream",
          default_data_netio->owner_name, nstrm_mode);
        (default_data_netio->abort)(nstrm);
      }
      break;

    case PR_NETIO_STRM_OTHR:
      if (othr_netio != NULL) {
        pr_trace_msg(trace_channel, 19, "using %s abort() for other %s stream",
          othr_netio->owner_name, nstrm_mode);
        (othr_netio->abort)(nstrm);

      } else {
        pr_trace_msg(trace_channel, 19, "using %s abort() for other %s stream",
          default_othr_netio->owner_name, nstrm_mode);
        (default_othr_netio->abort)(nstrm);
      }
      break;

    default:
      errno = EINVAL;
      return;
  }

  return;
}

int pr_netio_close(pr_netio_stream_t *nstrm) {
  int res = -1, xerrno = 0;
  const char *nstrm_mode;

  if (nstrm == NULL) {
    errno = EINVAL;
    return -1;
  }

  nstrm_mode = netio_stream_mode(nstrm->strm_mode);

  switch (nstrm->strm_type) {
    case PR_NETIO_STRM_CTRL:
      if (ctrl_netio != NULL) {
        pr_trace_msg(trace_channel, 19,
          "using %s close() for control %s stream", ctrl_netio->owner_name,
          nstrm_mode);
        res = (ctrl_netio->close)(nstrm);

      } else {
        pr_trace_msg(trace_channel, 19,
          "using %s close() for control %s stream",
          default_ctrl_netio->owner_name, nstrm_mode);
        res = (default_ctrl_netio->close)(nstrm);
      }
      xerrno = errno;
      break;

    case PR_NETIO_STRM_DATA:
      if (data_netio != NULL) {
        pr_trace_msg(trace_channel, 19, "using %s close() for data %s stream",
          data_netio->owner_name, nstrm_mode);
        res = (data_netio->close)(nstrm);

      } else {
        pr_trace_msg(trace_channel, 19, "using %s close() for data %s stream",
          default_data_netio->owner_name, nstrm_mode);
        res = (default_data_netio->close)(nstrm);
      }
      xerrno = errno;
      break;

    case PR_NETIO_STRM_OTHR:
      if (othr_netio != NULL) {
        pr_trace_msg(trace_channel, 19, "using %s close() for other %s stream",
          othr_netio->owner_name, nstrm_mode);
        res = (othr_netio->close)(nstrm);

      } else {
        pr_trace_msg(trace_channel, 19, "using %s close() for other %s stream",
          default_othr_netio->owner_name, nstrm_mode);
        res = (default_othr_netio->close)(nstrm);
      }
      xerrno = errno;
      break;

    default:
      errno = EPERM;
      return -1;
  }

  /* Make sure to scrub any buffered memory, too. */
  if (nstrm->strm_buf != NULL) {
    pr_buffer_t *pbuf;

    pbuf = nstrm->strm_buf;
    pr_memscrub(pbuf->buf, pbuf->buflen);
  }

  destroy_pool(nstrm->strm_pool);
  errno = xerrno;
  return res;
}

static int netio_lingering_close(pr_netio_stream_t *nstrm, long linger,
    int flags) {
  int res;
  const char *nstrm_mode;

  if (nstrm == NULL) {
    errno = EINVAL;
    return -1;
  }

  switch (nstrm->strm_type) {
    case PR_NETIO_STRM_CTRL:
    case PR_NETIO_STRM_DATA:
    case PR_NETIO_STRM_OTHR:
      break;

    default:
      errno = EPERM;
      return -1;
  }

  if (nstrm->strm_fd < 0) {
    /* Already closed. */
    return 0;
  }

  nstrm_mode = netio_stream_mode(nstrm->strm_mode);

  if (!(flags & NETIO_LINGERING_CLOSE_FL_NO_SHUTDOWN)) {
    pr_netio_shutdown(nstrm, 1);
  }

  if (nstrm->strm_fd >= 0) {
    struct timeval tv;
    fd_set rfds;
    time_t when = time(NULL) + linger;

    tv.tv_sec = linger;
    tv.tv_usec = 0L;

    /* Handle timers during reading, once selected for read this
     * should mean all buffers have been flushed and the receiving end
     * has closed.
     */
    while (TRUE) {
      run_schedule();

      FD_ZERO(&rfds);
      FD_SET(nstrm->strm_fd, &rfds);

      pr_trace_msg(trace_channel, 8,
        "lingering %lu %s before closing fd %d",
        (unsigned long) tv.tv_sec, tv.tv_sec != 1 ? "secs" : "sec",
        nstrm->strm_fd);

      res = select(nstrm->strm_fd+1, &rfds, NULL, NULL, &tv);
      if (res == -1) {
        if (errno == EINTR) {
          time_t now = time(NULL);
          pr_signals_handle();

          /* Still here? If the requested lingering interval hasn't passed,
           * continue lingering.  Reset the timeval struct's fields to
           * linger for the interval remaining in the given period of time.
           */
          if (now < when) {
            tv.tv_sec = when - now;
            tv.tv_usec = 0L;
            continue;
          }

        } else {
          nstrm->strm_errno = errno;
          return -1;
        }

      } else {
        if (FD_ISSET(nstrm->strm_fd, &rfds)) {
          pr_trace_msg(trace_channel, 8,
            "received data for reading on fd %d, ignoring", nstrm->strm_fd);
        }
      }

      break;
    }
  }

  pr_trace_msg(trace_channel, 8, "done lingering, closing fd %d",
    nstrm->strm_fd);

  switch (nstrm->strm_type) {
    case PR_NETIO_STRM_CTRL:
      if (ctrl_netio != NULL) {
        pr_trace_msg(trace_channel, 19,
          "using %s close() for control %s stream", ctrl_netio->owner_name,
          nstrm_mode);
        res = (ctrl_netio->close)(nstrm);

      } else {
        pr_trace_msg(trace_channel, 19,
          "using %s close() for control %s stream",
          default_ctrl_netio->owner_name, nstrm_mode);
        res = (default_ctrl_netio->close)(nstrm);
      }
      return res;

    case PR_NETIO_STRM_DATA:
      if (data_netio != NULL) {
        pr_trace_msg(trace_channel, 19, "using %s close() for data %s stream",
          data_netio->owner_name, nstrm_mode);
        res = (data_netio->close)(nstrm);

      } else {
        pr_trace_msg(trace_channel, 19, "using %s close() for data %s stream",
          default_data_netio->owner_name, nstrm_mode);
        res = (default_data_netio->close)(nstrm);
      }
      return res;

    case PR_NETIO_STRM_OTHR:
      if (othr_netio != NULL) {
        pr_trace_msg(trace_channel, 19, "using %s close() for other %s stream",
          othr_netio->owner_name, nstrm_mode);
        res = (othr_netio->close)(nstrm);

      } else {
        pr_trace_msg(trace_channel, 19, "using %s close() for other %s stream",
          default_othr_netio->owner_name, nstrm_mode);
        res = (default_othr_netio->close)(nstrm);
      }
      return res;
  }

  errno = EPERM;
  return -1;
}

int pr_netio_lingering_abort(pr_netio_stream_t *nstrm, long linger) {
  int res;

  if (nstrm == NULL) {
    errno = EINVAL;
    return -1;
  }

  switch (nstrm->strm_type) {
    case PR_NETIO_STRM_CTRL:
    case PR_NETIO_STRM_DATA:
    case PR_NETIO_STRM_OTHR:
      break;

    default:
      errno = EPERM;
      return -1;
  }

  /* Send an appropriate response code down the stream asynchronously. */
  pr_response_send_async(R_426, _("Transfer aborted. Data connection closed."));

  pr_netio_shutdown(nstrm, 1);

  if (nstrm->strm_fd >= 0) {
    fd_set rs;
    struct timeval tv;

    /* Wait for just a little while for the shutdown to take effect. */
    tv.tv_sec = 0L;
    tv.tv_usec = 300000L;

    while (TRUE) {
      run_schedule();

      FD_ZERO(&rs);
      FD_SET(nstrm->strm_fd, &rs);

      res = select(nstrm->strm_fd+1, &rs, NULL, NULL, &tv);
      if (res == -1) {
        if (errno == EINTR) {
          pr_signals_handle();

          /* Linger some more. */
          tv.tv_sec = 0L;
          tv.tv_usec = 300000L;
          continue;

        } else {
          nstrm->strm_errno = errno;
          return -1;
        }
      }

      break;
    }
  }

  nstrm->strm_flags |= PR_NETIO_SESS_ABORT;

  /* Now continue with a normal lingering close. */
  return netio_lingering_close(nstrm, linger,
    NETIO_LINGERING_CLOSE_FL_NO_SHUTDOWN);  
}

int pr_netio_lingering_close(pr_netio_stream_t *nstrm, long linger) {
  return netio_lingering_close(nstrm, linger, 0);
}

pr_netio_stream_t *pr_netio_open(pool *parent_pool, int strm_type, int fd,
    int mode) {
  pr_netio_stream_t *nstrm = NULL, *res = NULL;
  const char *nstrm_mode;

  if (parent_pool == NULL) {
    errno = EINVAL;
    return NULL;
  }

  /* Create a new stream object, then pass that the NetIO open handler. */
  nstrm = netio_stream_alloc(parent_pool);
  nstrm_mode = netio_stream_mode(mode);

  switch (strm_type) {
    case PR_NETIO_STRM_CTRL:
      nstrm->strm_type = PR_NETIO_STRM_CTRL;
      nstrm->strm_mode = mode;

      if (ctrl_netio != NULL) {
        if (pr_table_add(nstrm->notes, pstrdup(nstrm->strm_pool, "core.netio"),
            ctrl_netio, sizeof(pr_netio_t *)) < 0) {
          pr_trace_msg(trace_channel, 9,
            "error stashing 'core.netio' note for ctrl stream: %s",
            strerror(errno));
        }
        pr_trace_msg(trace_channel, 19, "using %s open() for control %s stream",
          ctrl_netio->owner_name, nstrm_mode);
        res = (ctrl_netio->open)(nstrm, fd, mode);
        if (res != NULL) {
          res->strm_netio = ctrl_netio;
        }

      } else {
        if (pr_table_add(nstrm->notes, pstrdup(nstrm->strm_pool, "core.netio"),
            default_ctrl_netio, sizeof(pr_netio_t *)) < 0) {
          pr_trace_msg(trace_channel, 9,
            "error stashing 'core.netio' note for ctrl stream: %s",
            strerror(errno));
        }
        pr_trace_msg(trace_channel, 19, "using %s open() for control %s stream",
          default_ctrl_netio->owner_name, nstrm_mode);
        res = (default_ctrl_netio->open)(nstrm, fd, mode);
        if (res != NULL) {
          res->strm_netio = default_ctrl_netio;
        }
      }
      break;

    case PR_NETIO_STRM_DATA:
      nstrm->strm_type = PR_NETIO_STRM_DATA;
      nstrm->strm_mode = mode;

      if (data_netio != NULL) {
        if (pr_table_add(nstrm->notes, pstrdup(nstrm->strm_pool, "core.netio"),
            data_netio, sizeof(pr_netio_t *)) < 0) {
          pr_trace_msg(trace_channel, 9,
            "error stashing 'core.netio' note for data stream: %s",
            strerror(errno));
        }
        pr_trace_msg(trace_channel, 19, "using %s open() for data %s stream",
          data_netio->owner_name, nstrm_mode);
        res = (data_netio->open)(nstrm, fd, mode);
        if (res != NULL) {
          res->strm_netio = data_netio;
        }

      } else {
        if (pr_table_add(nstrm->notes, pstrdup(nstrm->strm_pool, "core.netio"),
            default_data_netio, sizeof(pr_netio_t *)) < 0) {
          pr_trace_msg(trace_channel, 9,
            "error stashing 'core.netio' note for data stream: %s",
            strerror(errno));
        }
        pr_trace_msg(trace_channel, 19, "using %s open() for data %s stream",
          default_data_netio->owner_name, nstrm_mode);
        res = (default_data_netio->open)(nstrm, fd, mode);
        if (res != NULL) {
          res->strm_netio = default_data_netio;
        }
      }
      break;

    case PR_NETIO_STRM_OTHR:
      nstrm->strm_type = PR_NETIO_STRM_OTHR;
      nstrm->strm_mode = mode;

      if (othr_netio != NULL) {
        if (pr_table_add(nstrm->notes, pstrdup(nstrm->strm_pool, "core.netio"),
            othr_netio, sizeof(pr_netio_t *)) < 0) {
          pr_trace_msg(trace_channel, 9,
            "error stashing 'core.netio' note for othr stream: %s",
            strerror(errno));
        }
        pr_trace_msg(trace_channel, 19, "using %s open() for other %s stream",
          othr_netio->owner_name, nstrm_mode);
        res = (othr_netio->open)(nstrm, fd, mode);
        if (res != NULL) {
          res->strm_netio = othr_netio;
        }

      } else {
        if (pr_table_add(nstrm->notes, pstrdup(nstrm->strm_pool, "core.netio"),
            default_othr_netio, sizeof(pr_netio_t *)) < 0) {
          pr_trace_msg(trace_channel, 9,
            "error stashing 'core.netio' note for othr stream: %s",
            strerror(errno));
        }
        pr_trace_msg(trace_channel, 19, "using %s open() for other %s stream",
          default_othr_netio->owner_name, nstrm_mode);
        res = (default_othr_netio->open)(nstrm, fd, mode);
        if (res != NULL) {
          res->strm_netio = default_othr_netio;
        }
      }
      break;

    default:
      destroy_pool(nstrm->strm_pool);
      nstrm->strm_pool = NULL;
      errno = EINVAL;
      res = NULL;
  }

  return res;
}

pr_netio_stream_t *pr_netio_reopen(pr_netio_stream_t *nstrm, int fd, int mode) {
  pr_netio_stream_t *res;
  const char *nstrm_mode;

  if (nstrm == NULL) {
    errno = EINVAL;
    return NULL;
  }

  nstrm_mode = netio_stream_mode(mode);

  switch (nstrm->strm_type) {
    case PR_NETIO_STRM_CTRL:
      if (ctrl_netio != NULL) {
        pr_trace_msg(trace_channel, 19,
          "using %s reopen() for control %s stream", ctrl_netio->owner_name,
          nstrm_mode);
        res = (ctrl_netio->reopen)(nstrm, fd, mode);

      } else {
        pr_trace_msg(trace_channel, 19,
          "using %s reopen() for control %s stream",
          default_ctrl_netio->owner_name, nstrm_mode);
        res = (default_ctrl_netio->reopen)(nstrm, fd, mode);
      }
      return res;

    case PR_NETIO_STRM_DATA:
      if (data_netio != NULL) {
        pr_trace_msg(trace_channel, 19, "using %s reopen() for data %s stream",
          data_netio->owner_name, nstrm_mode);
        res = (data_netio->reopen)(nstrm, fd, mode);

      } else {
        pr_trace_msg(trace_channel, 19, "using %s reopen() for data %s stream",
          default_data_netio->owner_name, nstrm_mode);
        res = (default_data_netio->reopen)(nstrm, fd, mode);
      }
      return res;

    case PR_NETIO_STRM_OTHR:
      if (othr_netio != NULL) {
        pr_trace_msg(trace_channel, 19, "using %s reopen() for other %s stream",
          othr_netio->owner_name, nstrm_mode);
        res = (othr_netio->reopen)(nstrm, fd, mode);

      } else {
        pr_trace_msg(trace_channel, 19, "using %s reopen() for other %s stream",
          default_othr_netio->owner_name, nstrm_mode);
        res = (default_othr_netio->reopen)(nstrm, fd, mode);
      }
      return res;
  }

  errno = EPERM;
  return NULL;
}

void pr_netio_reset_poll_interval(pr_netio_stream_t *nstrm) {
  if (nstrm == NULL) {
    errno = EINVAL;
    return;
  }

  /* Simply clear the "interruptible" flag. */
  nstrm->strm_flags &= ~PR_NETIO_SESS_INTR;
}

void pr_netio_set_poll_interval(pr_netio_stream_t *nstrm, unsigned int secs) {
  if (nstrm == NULL) {
    return;
  }

  nstrm->strm_flags |= PR_NETIO_SESS_INTR;
  nstrm->strm_interval = secs;
}

int pr_netio_poll(pr_netio_stream_t *nstrm) {
  int res = 0, xerrno = 0;
  const char *nstrm_mode;

  /* Sanity checks. */
  if (nstrm == NULL) {
    errno = EINVAL;
    return -1;
  }

  if (nstrm->strm_fd == -1) {
    errno = EBADF;
    return -1;
  }

  /* Has this stream been aborted? */
  if (nstrm->strm_flags & PR_NETIO_SESS_ABORT) {
    nstrm->strm_flags &= ~PR_NETIO_SESS_ABORT;
    return 1;
  }

  nstrm_mode = netio_stream_mode(nstrm->strm_mode);

  while (TRUE) {
    run_schedule();
    pr_signals_handle();

    switch (nstrm->strm_type) {
      case PR_NETIO_STRM_CTRL:
        if (ctrl_netio != NULL) {
          pr_trace_msg(trace_channel, 19,
            "using %s poll() for control %s stream", ctrl_netio->owner_name,
            nstrm_mode);
          res = (ctrl_netio->poll)(nstrm);

        } else {
          pr_trace_msg(trace_channel, 19,
            "using %s poll() for control %s stream",
            default_ctrl_netio->owner_name, nstrm_mode);
          res = (default_ctrl_netio->poll)(nstrm);
        }
        break;

      case PR_NETIO_STRM_DATA:
        if (data_netio != NULL) {
          pr_trace_msg(trace_channel, 19,
            "using %s poll() for data %s stream", data_netio->owner_name,
            nstrm_mode);
          res = (data_netio->poll)(nstrm);

        } else {
          pr_trace_msg(trace_channel, 19,
            "using %s poll() for data %s stream",
            default_data_netio->owner_name, nstrm_mode);
          res = (default_data_netio->poll)(nstrm);
        }
        break;

      case PR_NETIO_STRM_OTHR:
        if (othr_netio != NULL) {
          pr_trace_msg(trace_channel, 19,
            "using %s poll() for other %s stream", othr_netio->owner_name,
            nstrm_mode);
          res = (othr_netio->poll)(nstrm);

        } else {
          pr_trace_msg(trace_channel, 19,
            "using %s poll() for other %s stream",
            default_othr_netio->owner_name, nstrm_mode);
          res = (default_othr_netio->poll)(nstrm);
        }
        break;
    }

    if (res == -1) {
      xerrno = errno;
      if (xerrno == EINTR) {
        if (nstrm->strm_flags & PR_NETIO_SESS_ABORT) {
          nstrm->strm_flags &= ~PR_NETIO_SESS_ABORT;
          return 1;
        }

        if (nstrm->strm_flags & PR_NETIO_SESS_INTR) {
          errno = nstrm->strm_errno = xerrno;
          return -1;
        }

        /* Otherwise, restart the call */
        pr_signals_handle();
        continue;
      }

      /* Some other error occurred */
      nstrm->strm_errno = xerrno;

      /* If this is the control stream, and the error indicates a
       * broken pipe (i.e. the client went away), AND there is a data
       * transfer is progress, abort the transfer.
       */
      if (xerrno == EPIPE &&
          nstrm->strm_type == PR_NETIO_STRM_CTRL &&
          (session.sf_flags & SF_XFER)) {
        pr_trace_msg(trace_channel, 5,
          "received EPIPE on control connection, setting 'aborted' "
          "session flag");
        session.sf_flags |= SF_ABORT;
      }

      errno = nstrm->strm_errno;
      return -1;
    }

    if (res == 0) {
      /* In case the kernel doesn't support interrupted syscalls. */
      if (nstrm->strm_flags & PR_NETIO_SESS_ABORT) {
        nstrm->strm_flags &= ~PR_NETIO_SESS_ABORT;
        return 1;
      }

      /* If the stream has been marked as "interruptible", AND the
       * poll interval is zero seconds (meaning a true poll, not blocking),
       * then return here.
       */
      if ((nstrm->strm_flags & PR_NETIO_SESS_INTR) &&
          nstrm->strm_interval == 0) {
        errno = EOF;
        return -1;
      }

      continue;
    }

    break;
  }

  return 0;
}

int pr_netio_postopen(pr_netio_stream_t *nstrm) {
  int res;
  const char *nstrm_mode;

  if (nstrm == NULL) {
    errno = EINVAL;
    return -1;
  }

  nstrm_mode = netio_stream_mode(nstrm->strm_mode);

  switch (nstrm->strm_type) {
    case PR_NETIO_STRM_CTRL:
      if (ctrl_netio != NULL) {
        pr_trace_msg(trace_channel, 19,
          "using %s postopen() for control %s stream", ctrl_netio->owner_name,
          nstrm_mode);
        res = (ctrl_netio->postopen)(nstrm);

      } else {
        pr_trace_msg(trace_channel, 19,
          "using %s postopen() for control %s stream",
          default_ctrl_netio->owner_name, nstrm_mode);
        res = (default_ctrl_netio->postopen)(nstrm);
      }
      return res;

    case PR_NETIO_STRM_DATA:
      if (data_netio != NULL) {
        pr_trace_msg(trace_channel, 19,
          "using %s postopen() for data %s stream", data_netio->owner_name,
          nstrm_mode);
        res = (data_netio->postopen)(nstrm);

      } else {
        pr_trace_msg(trace_channel, 19,
          "using %s postopen() for data %s stream",
          default_data_netio->owner_name, nstrm_mode);
        res = (default_data_netio->postopen)(nstrm);
      }
      return res;

    case PR_NETIO_STRM_OTHR:
      if (othr_netio != NULL) {
        pr_trace_msg(trace_channel, 19,
          "using %s postopen() for other %s stream", othr_netio->owner_name,
          nstrm_mode);
        res = (othr_netio->postopen)(nstrm);

      } else {
        pr_trace_msg(trace_channel, 19,
          "using %s postopen() for other %s stream",
          default_othr_netio->owner_name, nstrm_mode);
        res = (default_othr_netio->postopen)(nstrm);
      }
      return res;
  }

  errno = EPERM;
  return -1;
}

int pr_netio_vprintf(pr_netio_stream_t *nstrm, const char *fmt, va_list msg) {
  char buf[PR_RESPONSE_BUFFER_SIZE] = {'\0'};

  pr_vsnprintf(buf, sizeof(buf), fmt, msg);
  buf[sizeof(buf)-1] = '\0';

  return pr_netio_write(nstrm, buf, strlen(buf));
}

int pr_netio_printf(pr_netio_stream_t *nstrm, const char *fmt, ...) {
  int res;
  va_list msg;

  if (!nstrm) {
    errno = EINVAL;
    return -1;
  }

  va_start(msg, fmt);
  res = pr_netio_vprintf(nstrm, fmt, msg);
  va_end(msg);

  return res;
}

int pr_netio_printf_async(pr_netio_stream_t *nstrm, char *fmt, ...) {
  va_list msg;
  char buf[PR_RESPONSE_BUFFER_SIZE] = {'\0'};

  if (!nstrm) {
    errno = EINVAL;
    return -1;
  }

  va_start(msg, fmt);
  pr_vsnprintf(buf, sizeof(buf), fmt, msg);
  va_end(msg);
  buf[sizeof(buf)-1] = '\0';

  return pr_netio_write_async(nstrm, buf, strlen(buf));
}

int pr_netio_write(pr_netio_stream_t *nstrm, char *buf, size_t buflen) {
  int bwritten = 0, total = 0;
  const char *nstrm_mode;
  pr_buffer_t *pbuf;
  pool *tmp_pool;

  /* Sanity check */
  if (nstrm == NULL ||
      buf == NULL ||
      buflen == 0) {
    errno = EINVAL;
    return -1;
  }

  if (nstrm->strm_fd == -1) {
    errno = (nstrm->strm_errno ? nstrm->strm_errno : EBADF);
    return -1;
  }

  nstrm_mode = netio_stream_mode(nstrm->strm_mode);

  /* Before we send out the data to the client, generate an event
   * for any listeners which may want to examine this data.  To do this, we
   * need to allocate a pr_buffer_t for sending the buffer data to the
   * listeners.
   *
   * We could just use nstrm->strm_pool, but for a long-lived control
   * connection, this would amount to a slow memory increase.  So instead,
   * we create a subpool from the stream's pool, and allocate the
   * pr_buffer_t out of that.  Then simply destroy the subpool when done.
   */

  tmp_pool = make_sub_pool(nstrm->strm_pool);
  pbuf = pcalloc(tmp_pool, sizeof(pr_buffer_t));
  pbuf->buf = buf;
  pbuf->buflen = buflen;
  pbuf->current = pbuf->buf;
  pbuf->remaining = 0;

  switch (nstrm->strm_type) {
    case PR_NETIO_STRM_CTRL:
      pr_event_generate("core.ctrl-write", pbuf);
      break;

    case PR_NETIO_STRM_DATA:
      pr_event_generate("core.data-write", pbuf);
      break;

    case PR_NETIO_STRM_OTHR:
      pr_event_generate("core.othr-write", pbuf);
      break;
  }

  /* The event listeners may have changed the data to write out. */
  buf = pbuf->buf;
  buflen = pbuf->buflen - pbuf->remaining;
  destroy_pool(tmp_pool);

  while (buflen) {

    switch (pr_netio_poll(nstrm)) {
      case 1:
        /* pr_netio_poll() returns 1 only if the stream has been aborted. */
        errno = ECONNABORTED;
        return -2;

      case -1:
        return -1;

      default:
        /* We have to potentially restart here as well, in case we get EINTR. */
        do {
          pr_signals_handle(); 
          run_schedule();

          switch (nstrm->strm_type) {
            case PR_NETIO_STRM_CTRL:
              if (ctrl_netio != NULL) {
                pr_trace_msg(trace_channel, 19,
                  "using %s write() for control %s stream",
                  ctrl_netio->owner_name, nstrm_mode);
                bwritten = (ctrl_netio->write)(nstrm, buf, buflen);

              } else {
                pr_trace_msg(trace_channel, 19,
                  "using %s write() for control %s stream",
                  default_ctrl_netio->owner_name, nstrm_mode);
                bwritten = (default_ctrl_netio->write)(nstrm, buf, buflen);
              }
              break;

            case PR_NETIO_STRM_DATA:
              if (XFER_ABORTED) {
                break;
              }

              if (data_netio != NULL) {
                pr_trace_msg(trace_channel, 19,
                  "using %s write() for data %s stream", data_netio->owner_name,
                  nstrm_mode);
                bwritten = (data_netio->write)(nstrm, buf, buflen);

              } else {
                pr_trace_msg(trace_channel, 19,
                  "using %s write() for data %s stream",
                  default_data_netio->owner_name, nstrm_mode);
                bwritten = (default_data_netio->write)(nstrm, buf, buflen);
              }
              break;

            case PR_NETIO_STRM_OTHR:
              if (othr_netio != NULL) {
                pr_trace_msg(trace_channel, 19,
                  "using %s write() for other %s stream",
                  othr_netio->owner_name, nstrm_mode);
                bwritten = (othr_netio->write)(nstrm, buf, buflen);

              } else {
                pr_trace_msg(trace_channel, 19,
                  "using %s write() for other %s stream",
                  default_othr_netio->owner_name, nstrm_mode);
                bwritten = (default_othr_netio->write)(nstrm, buf, buflen);
              }
              break;
          }

        } while (bwritten == -1 && errno == EINTR);
        break;
    }

    if (bwritten == -1) {
      nstrm->strm_errno = errno;
      return -1;
    }

    buf += bwritten;
    total += bwritten;
    buflen -= bwritten;
  }

  session.total_raw_out += total;
  return total;
}

int pr_netio_write_async(pr_netio_stream_t *nstrm, char *buf, size_t buflen) {
  int bwritten = 0, flags = 0, total = 0;
  const char *nstrm_mode;
  pr_buffer_t *pbuf;
  pool *tmp_pool;

  /* Sanity check */
  if (nstrm == NULL) {
    errno = EINVAL;
    return -1;
  }

  if (nstrm->strm_fd == -1) {
    errno = (nstrm->strm_errno ? nstrm->strm_errno : EBADF);
    return -1;
  }

  /* Prepare the descriptor for nonblocking IO. */
  flags = fcntl(nstrm->strm_fd, F_GETFL);
  if (flags < 0) {
    return -1;
  }

  if (fcntl(nstrm->strm_fd, F_SETFL, flags|O_NONBLOCK) < 0) {
    return -1;
  }

  nstrm_mode = netio_stream_mode(nstrm->strm_mode);

  /* Before we send out the data to the client, generate an event
   * for any listeners which may want to examine this data.
   */

  tmp_pool = make_sub_pool(nstrm->strm_pool);
  pbuf = pcalloc(tmp_pool, sizeof(pr_buffer_t));
  pbuf->buf = buf;
  pbuf->buflen = buflen;
  pbuf->current = pbuf->buf;
  pbuf->remaining = 0;

  switch (nstrm->strm_type) {
    case PR_NETIO_STRM_CTRL:
      pr_event_generate("core.ctrl-write", pbuf);
      break;

    case PR_NETIO_STRM_DATA:
      pr_event_generate("core.data-write", pbuf);
      break;

    case PR_NETIO_STRM_OTHR:
      pr_event_generate("core.othr-write", pbuf);
      break;
  }

  /* The event listeners may have changed the data to write out. */
  buf = pbuf->buf;
  buflen = pbuf->buflen - pbuf->remaining;
  destroy_pool(tmp_pool);

  while (buflen) {
    do {

      /* Do NOT check for XFER_ABORTED here.  After a client aborts a
       * transfer, proftpd still needs to send the 426 response code back
       * to the client via the control connection; checking for XFER_ABORTED
       * here would block that response code sending, which in turn causes
       * problems for clients waiting for that response code.
       */

      pr_signals_handle();

      switch (nstrm->strm_type) {
        case PR_NETIO_STRM_CTRL:
          if (ctrl_netio != NULL) {
            pr_trace_msg(trace_channel, 19,
              "using %s write() for control %s stream", ctrl_netio->owner_name,
              nstrm_mode);
            bwritten = (ctrl_netio->write)(nstrm, buf, buflen);

          } else {
            pr_trace_msg(trace_channel, 19,
              "using %s write() for control %s stream",
              default_ctrl_netio->owner_name, nstrm_mode);
            bwritten = (default_ctrl_netio->write)(nstrm, buf, buflen);
          }
          break;

        case PR_NETIO_STRM_DATA:
          if (data_netio != NULL) {
            pr_trace_msg(trace_channel, 19,
              "using %s write() for data %s stream", data_netio->owner_name,
              nstrm_mode);
            bwritten = (data_netio->write)(nstrm, buf, buflen);

          } else {
            pr_trace_msg(trace_channel, 19,
              "using %s write() for data %s stream",
              default_data_netio->owner_name, nstrm_mode);
            bwritten = (default_data_netio->write)(nstrm, buf, buflen);
          }
          break;

        case PR_NETIO_STRM_OTHR:
          if (othr_netio != NULL) {
            pr_trace_msg(trace_channel, 19,
              "using %s write() for other %s stream", othr_netio->owner_name,
              nstrm_mode);
            bwritten = (othr_netio->write)(nstrm, buf, buflen);

          } else {
            pr_trace_msg(trace_channel, 19,
              "using %s write() for other %s stream",
              default_othr_netio->owner_name, nstrm_mode);
            bwritten = (default_othr_netio->write)(nstrm, buf, buflen);
          }
          break;
      }

    } while (bwritten == -1 && errno == EINTR);

    if (bwritten < 0) {
      nstrm->strm_errno = errno;
      (void) fcntl(nstrm->strm_fd, F_SETFL, flags);

      if (nstrm->strm_errno == EWOULDBLOCK) {
        /* Give up ... */
        return total;
      }

      return -1;
    }

    buf += bwritten;
    total += bwritten;
    buflen -= bwritten;
  }

  (void) fcntl(nstrm->strm_fd, F_SETFL, flags);
  return total;
}

int pr_netio_read(pr_netio_stream_t *nstrm, char *buf, size_t buflen,
    int bufmin) {
  int bread = 0, total = 0;
  const char *nstrm_mode;
  pr_buffer_t *pbuf;
  pool *tmp_pool;

  /* Sanity check. */
  if (nstrm == NULL ||
      buf == NULL ||
      buflen == 0) {
    errno = EINVAL;
    return -1;
  }

  if (nstrm->strm_fd == -1) {
    errno = (nstrm->strm_errno ? nstrm->strm_errno : EBADF);
    return -1;
  }

  if (bufmin < 1) {
    bufmin = 1;
  }

  if ((size_t) bufmin > buflen) {
    bufmin = buflen;
  }

  nstrm_mode = netio_stream_mode(nstrm->strm_mode);

  while (bufmin > 0) {
    polling:

    switch (pr_netio_poll(nstrm)) {
      case 1:
        return -2;

      case -1:
        return -1;

      default:
        do {
          pr_signals_handle();

          run_schedule();

          switch (nstrm->strm_type) {
            case PR_NETIO_STRM_CTRL:
              if (ctrl_netio != NULL) {
                pr_trace_msg(trace_channel, 19,
                  "using %s read() for control %s stream",
                  ctrl_netio->owner_name, nstrm_mode);
                bread = (ctrl_netio->read)(nstrm, buf, buflen);

              } else {
                pr_trace_msg(trace_channel, 19,
                  "using %s read() for control %s stream",
                  default_ctrl_netio->owner_name, nstrm_mode);
                bread = (default_ctrl_netio->read)(nstrm, buf, buflen);
              }
              break;

            case PR_NETIO_STRM_DATA:
              if (XFER_ABORTED) {
                break;
              }

              if (data_netio != NULL) {
                pr_trace_msg(trace_channel, 19,
                  "using %s read() for data %s stream", data_netio->owner_name,
                  nstrm_mode);
                bread = (data_netio->read)(nstrm, buf, buflen);

              } else {
                pr_trace_msg(trace_channel, 19,
                  "using %s read() for data %s stream",
                  default_data_netio->owner_name, nstrm_mode);
                bread = (default_data_netio->read)(nstrm, buf, buflen);
              }
              break;

            case PR_NETIO_STRM_OTHR:
              if (othr_netio != NULL) {
                pr_trace_msg(trace_channel, 19,
                  "using %s read() for other %s stream",
                  othr_netio->owner_name, nstrm_mode);
                bread = (othr_netio->read)(nstrm, buf, buflen);

              } else {
                pr_trace_msg(trace_channel, 19,
                  "using %s read() for other %s stream",
                  default_othr_netio->owner_name, nstrm_mode);
                bread = (default_othr_netio->read)(nstrm, buf, buflen);
              }
              break;
          }

#ifdef EAGAIN
	  if (bread == -1 &&
              errno == EAGAIN) {
            int xerrno = EAGAIN;

            /* Treat this as an interrupted call, call pr_signals_handle()
             * (which will delay for a few msecs because of EINTR), and try
             * again.
             *
             * This should avoid a tightly spinning loop if read(2) returns
             * EAGAIN, as on a data transfer (Bug#3639).
             */

            errno = EINTR;
            pr_signals_handle();

            errno = xerrno;
            goto polling;
          }
#endif

        } while (bread == -1 && errno == EINTR);
        break;
    }

    if (bread == -1) {
      nstrm->strm_errno = errno;
      return -1;
    }

    /* EOF? */
    if (bread == 0) {
      if (nstrm->strm_type == PR_NETIO_STRM_CTRL) {
        pr_trace_msg(trace_channel, 7,
          "read %d bytes from control stream fd %d, handling as EOF", bread,
          nstrm->strm_fd);
      }

      nstrm->strm_errno = 0;
      errno = EOF;
      break;
    }

    /* Before we provide the data from the client, generate an event
     * for any listeners which may want to examine this data.  To do this, we
     * need to allocate a pr_buffer_t for sending the buffer data to the
     * listeners.
     *
     * We could just use nstrm->strm_pool, but for a long-lived control
     * connection, this would amount to a slow memory increase.  So instead,
     * we create a subpool from the stream's pool, and allocate the
     * pr_buffer_t out of that.  Then simply destroy the subpool when done.
     */

    tmp_pool = make_sub_pool(nstrm->strm_pool);
    pbuf = pcalloc(tmp_pool, sizeof(pr_buffer_t));
    pbuf->buf = buf;
    pbuf->buflen = bread;
    pbuf->current = pbuf->buf;
    pbuf->remaining = 0;

    switch (nstrm->strm_type) {
      case PR_NETIO_STRM_CTRL:
        pr_event_generate("core.ctrl-read", pbuf);
        break;

      case PR_NETIO_STRM_DATA:
        pr_event_generate("core.data-read", pbuf);
        break;

      case PR_NETIO_STRM_OTHR:
        pr_event_generate("core.othr-read", pbuf);
        break;
    }

    /* The event listeners may have changed the data read in out. */
    buf = pbuf->buf;
    bread = pbuf->buflen - pbuf->remaining;
    destroy_pool(tmp_pool);

    buf += bread;
    total += bread;
    bufmin -= bread;
    buflen -= bread;
  }

  session.total_raw_in += total;
  return total;
}

int pr_netio_shutdown(pr_netio_stream_t *nstrm, int how) {
  int res = -1;
  const char *nstrm_mode;

  if (nstrm == NULL) {
    errno = EINVAL;
    return -1;
  }

  nstrm_mode = netio_stream_mode(nstrm->strm_mode);

  switch (nstrm->strm_type) {
    case PR_NETIO_STRM_CTRL:
      if (ctrl_netio != NULL) {
        pr_trace_msg(trace_channel, 19,
          "using %s shutdown() for control %s stream", ctrl_netio->owner_name,
          nstrm_mode);
        res = (ctrl_netio->shutdown)(nstrm, how);

      } else {
        pr_trace_msg(trace_channel, 19,
          "using %s shutdown() for control %s stream",
          default_ctrl_netio->owner_name, nstrm_mode);
        res = (default_ctrl_netio->shutdown)(nstrm, how);
      }
      return res;

    case PR_NETIO_STRM_DATA:
      if (data_netio != NULL) {
        pr_trace_msg(trace_channel, 19,
          "using %s shutdown() for data %s stream", data_netio->owner_name,
          nstrm_mode);
        res = (data_netio->shutdown)(nstrm, how);

      } else {
        pr_trace_msg(trace_channel, 19,
          "using %s shutdown() for data %s stream",
          default_data_netio->owner_name, nstrm_mode);
        res = (default_data_netio->shutdown)(nstrm, how);
      }
      return res;

    case PR_NETIO_STRM_OTHR:
      if (othr_netio != NULL) {
        pr_trace_msg(trace_channel, 19,
          "using %s shutdown() for other %s stream", othr_netio->owner_name,
          nstrm_mode);
        res = (othr_netio->shutdown)(nstrm, how);

      } else {
        pr_trace_msg(trace_channel, 19,
          "using %s shutdown() for other %s stream",
          default_othr_netio->owner_name, nstrm_mode);
        res = (default_othr_netio->shutdown)(nstrm, how);
      }
      return res;
  }

  errno = EPERM;
  return res;
}

char *pr_netio_gets(char *buf, size_t buflen, pr_netio_stream_t *nstrm) {
  char *bp = buf;
  int toread;
  pr_buffer_t *pbuf = NULL;

  if (nstrm == NULL ||
      buf == NULL ||
      buflen == 0) {
    errno = EINVAL;
    return NULL;
  }

  buflen--;

  if (nstrm->strm_buf) {
    pbuf = nstrm->strm_buf;

  } else {
    pbuf = pr_netio_buffer_alloc(nstrm);
  }

  while (buflen) {

    /* Is the buffer empty? */
    if (!pbuf->current ||
        pbuf->remaining == pbuf->buflen) {

      toread = pr_netio_read(nstrm, pbuf->buf,
        (buflen < pbuf->buflen ?  buflen : pbuf->buflen), 1);

      if (toread <= 0) {
        if (bp != buf) {
          *bp = '\0';
          return buf;
        }

        return NULL;
      }

      pbuf->remaining = pbuf->buflen - toread;
      pbuf->current = pbuf->buf;

      pbuf->remaining = pbuf->buflen - toread;
      pbuf->current = pbuf->buf;

      /* Before we begin iterating through the data read in from the
       * network, generate an event for any listeners which may want to
       * examine this data as well.
       */
      pr_event_generate("core.othr-read", pbuf);
    }

    toread = pbuf->buflen - pbuf->remaining;

    while (buflen && *pbuf->current != '\n' && toread--) {
      if (*pbuf->current & 0x80) {
        pbuf->current++;

      } else {
        *bp++ = *pbuf->current++;
        buflen--;
      }
      pbuf->remaining++;
    }

    if (buflen && toread && *pbuf->current == '\n') {
      buflen--;
      toread--;
      *bp++ = *pbuf->current++;
      pbuf->remaining++;
      break;
    }

    if (!toread) {
      pbuf->current = NULL;
    }
  }

  *bp = '\0';
  return buf;
}

static int telnet_mode = 0;

int pr_netio_telnet_gets2(char *buf, size_t bufsz,
    pr_netio_stream_t *in_nstrm, pr_netio_stream_t *out_nstrm) {
  char *bp = buf;
  unsigned char cp;
  int toread, handle_iac = TRUE, saw_newline = FALSE;
  pr_buffer_t *pbuf = NULL;
  size_t buflen = bufsz;

  if (buflen == 0 ||
      in_nstrm == NULL ||
      out_nstrm == NULL) {
    errno = EINVAL;
    return -1;
  }

#ifdef PR_USE_NLS
  handle_iac = pr_encode_supports_telnet_iac();
#endif /* PR_USE_NLS */

  buflen--;

  if (in_nstrm->strm_buf) {
    pbuf = in_nstrm->strm_buf;

  } else {
    pbuf = pr_netio_buffer_alloc(in_nstrm);
  }

  while (buflen > 0) {
    pr_signals_handle();

    /* Is the buffer empty? */
    if (pbuf->current == NULL ||
        pbuf->remaining == pbuf->buflen) {

      toread = pr_netio_read(in_nstrm, pbuf->buf,
        (buflen < pbuf->buflen ? buflen : pbuf->buflen), 1);

      if (toread <= 0) {
        if (bp != buf) {
          *bp = '\0';
          return (bufsz - buflen - 1);
        }

        return -1;
      }

      pbuf->remaining = pbuf->buflen - toread;
      pbuf->current = pbuf->buf;

      /* Before we begin iterating through the data read in from the
       * network, handing any Telnet characters and such, generate an event
       * for any listeners which may want to examine this data as well.
       */
      pr_event_generate("core.ctrl-read", pbuf);
    }

    toread = pbuf->buflen - pbuf->remaining;

    while (buflen > 0 &&
           toread > 0 &&
           (*pbuf->current != '\n' ||
            (*pbuf->current == '\n' && *(pbuf->current - 1) != '\r')) &&
           toread--) {
      pr_signals_handle();

      cp = *pbuf->current++;
      pbuf->remaining++;

      if (handle_iac == TRUE) {
        switch (telnet_mode) {
          case TELNET_IAC:
            switch (cp) {
              case TELNET_WILL:
              case TELNET_WONT:
              case TELNET_DO:
              case TELNET_DONT:
              case TELNET_IP:
              case TELNET_DM:
                /* Why do we do this crazy thing where we set the "telnet mode"
                 * to be the action, and let the while loop, on the next pass,
                 * handle that action?  It's because we don't know, right now,
                 * whether there actually a "next byte" in the input buffer.
                 * There _should_ be -- but we can't be sure.  And that next
                 * byte is needed for properly responding with WONT/DONT
                 * responses.
                 */
                telnet_mode = cp;
                continue;

              case TELNET_IAC:
                /* In this case, we know that the previous byte was TELNET_IAC,
                 * and that the current byte is another TELNET_IAC.  The
                 * first TELNET_IAC thus "escapes" the second, telling us
                 * that the current byte (TELNET_IAC) should be written out
                 * as is (Bug#3697).
                 */
                telnet_mode = 0;
                break;

              default:
                /* In this case, we know that the previous byte was TELNET_IAC,
                 * but the current byte is not a value we care about.  So
                 * write the TELNET_IAC into the output buffer, break out of
                 * of the switch, and let that handle the writing of the
                 * current byte into the output buffer.
                 */
                *bp++ = TELNET_IAC;
                buflen--;

                telnet_mode = 0;
                break;
            }
            break;

          case TELNET_WILL:
          case TELNET_WONT:
            pr_netio_printf(out_nstrm, "%c%c%c", TELNET_IAC, TELNET_DONT, cp);
            telnet_mode = 0;
            continue;

          case TELNET_DO:
          case TELNET_DONT:
            pr_netio_printf(out_nstrm, "%c%c%c", TELNET_IAC, TELNET_WONT, cp);
            telnet_mode = 0;
            continue;

          case TELNET_IP:
          case TELNET_DM:
          default:
            if (cp == TELNET_IAC) {
              telnet_mode = cp;
              continue;
            }
            break;
        }
      }

      /* In the situation where the previous byte was an IAC, we wrote IAC
       * into the output buffer, and decremented buflen (size of the output
       * buffer remaining).  Thus we need to check here if buflen is zero,
       * before trying to decrement buflen again (and possibly underflowing
       * the buflen size_t data type).
       */
      if (buflen == 0) {
        break;
      }

      *bp++ = cp;
      buflen--;
    }

    if (buflen > 0 &&
        toread > 0 &&
        *pbuf->current == '\n') {

      /* If the current character is LF, and the previous character we
       * copied was a CR, then strip the CR by overwriting it with the LF,
       * turning the copied data from Telnet CRLF line termination to
       * Unix LF line termination.
       */
      if (*(bp-1) == '\r') {
        /* We already decrement the buffer length for the CR; no need to
         * do it again since we are overwriting that CR.
         */
        *(bp-1) = *pbuf->current++;

      } else {
        *bp++ = *pbuf->current++;
        buflen--;
      }

      pbuf->remaining++;
      toread--;
      saw_newline = TRUE;
      break;
    }

    if (toread == 0) {
      /* No more input?  Set pbuf->current to null, so that at the top of
       * the loop, we read more.
       */
      pbuf->current = NULL;
    }
  }

  if (!saw_newline) {
    /* If we haven't seen a newline, then assume the client is deliberately
     * sending a too-long command, trying to exploit buffer sizes and make
     * the server make some possibly bad assumptions.
     */

    properly_terminated_prev_command = FALSE;
    errno = E2BIG;
    return -1;
  }

  if (!properly_terminated_prev_command) {
    properly_terminated_prev_command = TRUE;
    pr_log_pri(PR_LOG_NOTICE, "client sent too-long command, ignoring");
    errno = E2BIG;
    return -1;
  }

  properly_terminated_prev_command = TRUE;
  *bp = '\0';
  return (bufsz - buflen - 1);
}

char *pr_netio_telnet_gets(char *buf, size_t bufsz,
    pr_netio_stream_t *in_nstrm, pr_netio_stream_t *out_nstrm) {
  int res;

  res = pr_netio_telnet_gets2(buf, bufsz, in_nstrm, out_nstrm);
  if (res < 0) {
    return NULL;
  }

  return buf;
}

int pr_register_netio(pr_netio_t *netio, int strm_types) {

  if (netio == NULL) {
    pr_netio_t *default_netio = NULL;

    /* Only instantiate the default NetIO objects once, reusing the same
     * pointer.
     */
    if (default_ctrl_netio == NULL) {
      default_ctrl_netio = default_netio = pr_alloc_netio2(permanent_pool,
        NULL, NULL);
    }

    if (default_data_netio == NULL) {
      default_data_netio = default_netio ? default_netio :
        (default_netio = pr_alloc_netio2(permanent_pool, NULL, NULL));
    }

    if (default_othr_netio == NULL) {
      default_othr_netio = default_netio ? default_netio :
        (default_netio = pr_alloc_netio2(permanent_pool, NULL, NULL));
    }

    return 0;
  }

  if (!netio->abort || !netio->close || !netio->open || !netio->poll ||
      !netio->postopen || !netio->read || !netio->reopen ||
      !netio->shutdown || !netio->write) {
    errno = EINVAL;
    return -1;
  }

  if (strm_types & PR_NETIO_STRM_CTRL) {
    ctrl_netio = netio;
  }

  if (strm_types & PR_NETIO_STRM_DATA) {
    data_netio = netio;
  }

  if (strm_types & PR_NETIO_STRM_OTHR) {
    othr_netio = netio;
  }

  return 0;
}

int pr_unregister_netio(int strm_types) {
  if (!strm_types) {
    errno = EINVAL;
    return -1;
  }

  /* NOTE: consider using cleanups here in the future? */

  if (strm_types & PR_NETIO_STRM_CTRL) {
    ctrl_netio = NULL;
  }

  if (strm_types & PR_NETIO_STRM_DATA) {
    data_netio = NULL;
  }

  if (strm_types & PR_NETIO_STRM_OTHR) {
    othr_netio = NULL;
  }

  return 0;
}

pr_netio_t *pr_get_netio(int strm_type) {
  pr_netio_t *netio = NULL;

  if (strm_type == 0) {
    errno = EINVAL;
    return NULL;
  }

  switch (strm_type) {
    case PR_NETIO_STRM_CTRL:
      netio = ctrl_netio;
      break;

    case PR_NETIO_STRM_DATA:
      netio = data_netio;
      break;

    case PR_NETIO_STRM_OTHR:
      netio = othr_netio;
      break;

    default:
      errno = ENOENT;
  }

  return netio;
}

extern pid_t mpid;

pr_netio_t *pr_alloc_netio2(pool *parent_pool, module *owner,
    const char *owner_name) {
  pr_netio_t *netio = NULL;
  pool *netio_pool = NULL;

  if (parent_pool == NULL) {
    errno = EINVAL;
    return NULL;
  }

  netio_pool = make_sub_pool(parent_pool);

  /* If this is the daemon process, we are allocating a sub-pool from the
   * permanent_pool.  You might wonder why the daemon process needs netio
   * objects.  It doesn't, really -- but it's for use by all of the session
   * processes that will be forked.  They will be able to reuse the memory
   * already allocated for the main ctrl/data/other netios, as is.
   *
   * This being the case, we should label the sub-pool accordingly.
   */
  if (mpid == getpid()) {
    pr_pool_tag(netio_pool, "Shared Netio Pool");

  } else {
    pr_pool_tag(netio_pool, "netio pool");
  }

  netio = pcalloc(netio_pool, sizeof(pr_netio_t));
  netio->pool = netio_pool;
  netio->owner = owner;

  if (owner != NULL) {
    if (owner_name != NULL) {
      netio->owner_name = pstrdup(netio_pool, owner_name);

    } else {
      netio->owner_name = pstrdup(netio_pool, owner->name);
    }

  } else {
    if (owner_name != NULL) {
      netio->owner_name = owner_name;

    } else {
      netio->owner_name = "default";
    }
  }

  /* Set the default NetIO handlers to the core handlers. */
  netio->abort = core_netio_abort_cb;
  netio->close = core_netio_close_cb;
  netio->open = core_netio_open_cb;
  netio->poll = core_netio_poll_cb;
  netio->postopen = core_netio_postopen_cb;
  netio->read = core_netio_read_cb;
  netio->reopen = core_netio_reopen_cb;
  netio->shutdown = core_netio_shutdown_cb;
  netio->write = core_netio_write_cb;

  return netio;
}

pr_netio_t *pr_alloc_netio(pool *parent_pool) {
  return pr_alloc_netio2(parent_pool, NULL, NULL);
}

void init_netio(void) {
  signal(SIGPIPE, SIG_IGN);
  signal(SIGURG, SIG_IGN);

  pr_register_netio(NULL, 0);
}
