/*
 * ProFTPD: mod_deflate -- a module for supporting on-the-fly compression
 *
 * Copyright (c) 2004-2011 TJ Saunders
 *
 * 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, TJ Saunders 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.
 *
 * This is mod_deflate, contrib software for proftpd 1.3.x and above.
 * For more information contact TJ Saunders <tj@castaglia.org>.
 *
 * $Id: mod_deflate.c,v 1.8 2011/06/10 02:57:35 castaglia Exp $
 * $Libraries: -lz $
 */

#include <zlib.h>

#include "conf.h"
#include "privs.h"

#define MOD_DEFLATE_VERSION		"mod_deflate/0.5.5"

/* Make sure the version of proftpd is as necessary. */
#if PROFTPD_VERSION_NUMBER < 0x0001030201
# error "ProFTPD 1.3.2rc1 or later required"
#endif

static int deflate_enabled = FALSE;
static int deflate_engine = FALSE;
static int deflate_logfd = -1;
static pr_netio_t *deflate_netio = NULL;

/* Draft-recommended ZLIB defaults:
 *
 *  The following ZLIB [5] parameters are recommended for deflate
 *  transmission mode:
 *
 *     Compression level:   7
 *     Compression method:  Z_DEFLATED
 *     Window bits:         -15
 *     Memory level:        8
 *     Strategy:            Z_DEFAULT_STRATEGY
 */

#define MOD_DEFLATE_DEFAULT_COMPRESS_LEVEL		7
static int deflate_compression_level = MOD_DEFLATE_DEFAULT_COMPRESS_LEVEL;

#define MOD_DEFLATE_DEFAULT_MEM_LEVEL			8
static int deflate_mem_level = MOD_DEFLATE_DEFAULT_MEM_LEVEL;

#define MOD_DEFLATE_DEFAULT_STRATEGY			Z_DEFAULT_STRATEGY
static int deflate_strategy = MOD_DEFLATE_DEFAULT_STRATEGY;

#define MOD_DEFLATE_DEFAULT_WINDOW_BITS			15
static int deflate_window_bits = MOD_DEFLATE_DEFAULT_WINDOW_BITS;

/* The _ptr pointer always points to the start of the buffer; the _zbuf
 * pointer points to the current place within the buffer from which to read
 * data.
 */
static Byte *deflate_zbuf_ptr = NULL;
static Byte *deflate_zbuf = NULL;
static size_t deflate_zbuflen = 0;
static size_t deflate_zbufsz = 0;

static Byte *deflate_rbuf = NULL;
static size_t deflate_rbuflen = 0;
static size_t deflate_rbufsz = 0;

static int deflate_zerrno = 0;

static const char *trace_channel = "deflate";

static const char *deflate_zstrerror(int zerrno) {
  const char *zstr = "unknown";

  switch (zerrno) {
    case Z_OK:
      zstr = "OK";
      break;

    case Z_STREAM_END:
      return "End of stream";
      break;

    case Z_NEED_DICT:
      return "Need dictionary";
      break;

    case Z_ERRNO:
      zstr = strerror(errno);
      break;

    case Z_DATA_ERROR:
      zstr = "Data error";
      break;

    case Z_MEM_ERROR:
      zstr = "Memory error";
      break;

    case Z_BUF_ERROR:
      zstr = "Buffer error";
      break;

    case Z_VERSION_ERROR:
      zstr = "Version error";
      break;
  }

  return zstr;
}

/* NetIO callbacks
 */

static int deflate_netio_close_cb(pr_netio_stream_t *nstrm) {
  int res;

  if (nstrm->strm_type == PR_NETIO_STRM_DATA) {
    z_stream *zstrm = nstrm->strm_data;

    if (nstrm->strm_mode == PR_NETIO_IO_WR) {
      if (zstrm->total_in > 0) {
        float ratio;

        ratio = ((float) zstrm->total_out / (float) zstrm->total_in);

        (void) pr_log_writefile(deflate_logfd, MOD_DEFLATE_VERSION,
          "%s: deflated %lu bytes to %lu bytes (%0.2lf%% compression)",
          session.curr_cmd, zstrm->total_in, zstrm->total_out,
          (1.0 - ratio) * 100.0);
      }

      res = deflateEnd(zstrm);
      if (res != Z_OK) {
        pr_trace_msg(trace_channel, 3,
          "close: error closing deflating netio: [%d] %s", res,
          zstrm->msg ? zstrm->msg : deflate_zstrerror(res));

        (void) pr_log_writefile(deflate_logfd, MOD_DEFLATE_VERSION,
          "error closing deflating netio: [%d] %s", res,
          zstrm->msg ? zstrm->msg : deflate_zstrerror(res));
      }

    } else if (nstrm->strm_mode == PR_NETIO_IO_RD) {
      if (zstrm->total_in > 0) {
        float ratio;

        ratio = ((float) zstrm->total_in / (float) zstrm->total_out);

        (void) pr_log_writefile(deflate_logfd, MOD_DEFLATE_VERSION,
          "%s: inflated %lu bytes to %lu bytes (%0.2lf%% compression)",
          session.curr_cmd, zstrm->total_in, zstrm->total_out,
          (1.0 - ratio) * 100.0);
      }

      res = inflateEnd(zstrm);
      if (res != Z_OK) {
        pr_trace_msg(trace_channel, 3,
          "close: error closing inflating netio: [%d] %s", res,
          zstrm->msg ? zstrm->msg : deflate_zstrerror(res));

        (void) pr_log_writefile(deflate_logfd, MOD_DEFLATE_VERSION,
          "error closing inflating netio: [%d] %s", res,
          zstrm->msg ? zstrm->msg : deflate_zstrerror(res));
      }
    }
  }

  res = close(nstrm->strm_fd);
  nstrm->strm_fd = -1;
  nstrm->strm_data = NULL;

  return res;
}

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

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

  if (nstrm->strm_type == PR_NETIO_STRM_DATA) {
    int res;
    z_stream *zstrm;

    /* Set the initial ZLIB parameters. */
    zstrm = pcalloc(nstrm->strm_pool, sizeof(z_stream));
    zstrm->zalloc = Z_NULL;
    zstrm->zfree = Z_NULL;
    zstrm->opaque = Z_NULL;
    zstrm->next_in = Z_NULL;
    zstrm->next_out = Z_NULL;
    zstrm->avail_in = 0;
    zstrm->avail_out = 0;

    nstrm->strm_data = zstrm;

    memset(deflate_zbuf_ptr, '\0', deflate_zbufsz);
    deflate_zbuf = deflate_zbuf_ptr;

    if (nstrm->strm_mode == PR_NETIO_IO_WR) {
      /* Initialize the zlib data for deflation. */
      res = deflateInit2(zstrm, deflate_compression_level, Z_DEFLATED,
        deflate_window_bits, deflate_mem_level, deflate_strategy);

      switch (res) {
        case Z_OK:
          zstrm->next_out = deflate_zbuf;
          zstrm->avail_out = deflate_zbufsz;
          break;

        case Z_MEM_ERROR:
        case Z_STREAM_ERROR:
          pr_trace_msg(trace_channel, 3,
            "open: error initializing for deflation: [%d] %s", res,
            zstrm->msg ? zstrm->msg : deflate_zstrerror(res));

          (void) pr_log_writefile(deflate_logfd, MOD_DEFLATE_VERSION,
            "error initializing for deflation: [%d] %s", res,
            zstrm->msg ? zstrm->msg : deflate_zstrerror(res));

          errno = EINVAL;
          return NULL;
      }

    } else if (nstrm->strm_mode == PR_NETIO_IO_RD) {
      /* Initialize the zlib data for inflation.
       *
       * The magic number 32 here from the zlib.h documentation; it enables
       * the automatic header detection of zlib/gzip headers.
       */
      res = inflateInit2(zstrm, deflate_window_bits + 32);

      switch (res) {
        case Z_OK:
          zstrm->next_out = deflate_zbuf;
          zstrm->avail_out = deflate_zbufsz;
          break;

        case Z_MEM_ERROR:
        case Z_STREAM_ERROR:
          pr_trace_msg(trace_channel, 3,
            "open: error initializing for inflation: [%d] %s", res,
            zstrm->msg ? zstrm->msg : deflate_zstrerror(res));

          (void) pr_log_writefile(deflate_logfd, MOD_DEFLATE_VERSION,
            "error initializing for inflation: [%d] %s", res, 
            zstrm->msg ? zstrm->msg : deflate_zstrerror(res));

          errno = EINVAL;
          return NULL;
      }

      /* These are used by the read callback; ensure they are initialised to
       * zero before every read data transfer.
       */
      deflate_rbuflen = 0;
      deflate_zbuflen = 0;
    }
  }

  return nstrm;
}

static int deflate_netio_read_cb(pr_netio_stream_t *nstrm, char *buf,
    size_t bufsz) {

  if (bufsz == 0) {
    return 0;
  }

  if (nstrm->strm_type == PR_NETIO_STRM_DATA) {
    int datalen = 0, nread = 0, res, xerrno;
    size_t copylen = 0;
    z_stream *zstrm;

    zstrm = nstrm->strm_data;
    res = 0;

    /* If we have data leftover in deflate_zbuf, start by copying all of that
     * into the provided buffer.  Only read more data from the network and
     * inflate it when there's no leftover data.
     */

    if (deflate_zbuflen > 0) {
      if (bufsz >= deflate_zbuflen) {
        /* Excellent.  We can consume all of the data in the deflate_zbuf
         * buffer.
         */

        pr_trace_msg(trace_channel, 9, "read: returning %lu bytes of "
          "previously uncompressed data; no data read from client",
          (unsigned long) deflate_zbuflen);

        memcpy(buf, deflate_zbuf, deflate_zbuflen);
        res = deflate_zbuflen;

        /* Reset the pointer to the start of the buffer. */
        deflate_zbuf = deflate_zbuf_ptr;
        deflate_zbuflen = 0;

        /* Manually adjust the "raw" bytes in counter, so that it will
         * be accurate for %I logging.
         *
         * We subtract the number we are returning here, since our return
         * value will simply be added back to the counter in pr_netio_read().
         * And if our subtraction causes an underflow, it's still OK since
         * the subsequent addition will overflow, and get the value back to
         * what it should be.
         */
        session.total_raw_in -= res;

        return res;
      }

      /* The given buffer can't hold all of our already-inflated data; but
       * maybe it can hold some of it?
       */

      pr_trace_msg(trace_channel, 9, "read: returning %lu bytes of previously "
        "uncompressed data (of %lu bytes total); no data read from client",
        (unsigned long) bufsz, (unsigned long) deflate_zbuflen);

      memcpy(buf, deflate_zbuf, bufsz);
      res = bufsz;

      deflate_zbuf += bufsz;
      deflate_zbuflen -= bufsz;

      /* Manually adjust the "raw" bytes in counter, so that it will
       * be accurate for %I logging.
       *
       * We subtract the number we are returning here, since our return
       * value will simply be added back to the counter in pr_netio_read().
       * And if our subtraction causes an underflow, it's still OK since
       * the subsequent addition will overflow, and get the value back to
       * what it should be.
       */
      session.total_raw_in -= res;

      return res;
    }

    /* If we reach this point, then the deflate_zbuf buffer is empty of
     * uncompressed data.  We might have some compressed data left over from
     * the previous inflate() call that we need to process
     * (i.e. zstrm->avail_in > 0), though.
     *
     * Try to read more deta in from the network.  If we get no data, AND
     * zstrm->avail_in is zero, then we've reached EOF.  Otherwise, add the
     * new data to the inflator, and see if we can make some progress.
     */

    datalen = deflate_rbufsz - deflate_rbuflen;

    /* Read in some data from the stream's fd. */
    nread = read(nstrm->strm_fd, deflate_rbuf, datalen);
    if (nread < 0) {
      xerrno = errno;

      (void) pr_log_writefile(deflate_logfd, MOD_DEFLATE_VERSION,
        "error reading from socket %d: %s", nstrm->strm_fd, strerror(xerrno));

      errno = xerrno;
      return -1;
    }

    if (nread == 0) {
      if (zstrm->avail_in == 0) {
        /* EOF.  We know we can return zero here because the deflate_zbuf
         * is empty (see above comment), and we haven't read any more data
         * in from the network.
         */
        pr_trace_msg(trace_channel, 8,
          "read: read EOF from client, returning 0");
        return 0;
      }
    }

    pr_trace_msg(trace_channel, 9,
      "read: read %d bytes of compressed data from client", nread);

    /* Manually adjust the "raw" bytes in counter, so that it will
     * be accurate for %I logging.
     */
    session.total_raw_in += nread;

    if (zstrm->avail_in > 0) {
      pr_trace_msg(trace_channel, 9,
        "read: processing %d bytes of leftover compressed data from client, "
        "plus %d additional new bytes from client", zstrm->avail_in, nread);

    } else {
      pr_trace_msg(trace_channel, 9, "read: processing %d bytes from client",
        nread);
    }

    datalen = nread;
    zstrm->next_in = deflate_rbuf;
    zstrm->avail_in += datalen;

    copylen = 0;

    zstrm->next_out = deflate_zbuf;
    zstrm->avail_out = deflate_zbufsz;

    pr_trace_msg(trace_channel, 19,
      "read: pre-inflate zstream state: avail_in = %d, avail_out = %d",
      zstrm->avail_in, zstrm->avail_out);

    deflate_zerrno = inflate(zstrm, Z_SYNC_FLUSH);
    xerrno = errno;

    pr_trace_msg(trace_channel, 19,
      "read: post-inflate zstream state: avail_in = %d, avail_out = %d "
      "(zerrno = %s)", zstrm->avail_in, zstrm->avail_out,
      deflate_zstrerror(deflate_zerrno));

    errno = xerrno;

    switch (deflate_zerrno) {
      case Z_OK:
      case Z_STREAM_END:
        copylen = deflate_zbufsz - zstrm->avail_out;

        /* Allocate more space for the data if necessary. */
        if ((deflate_zbuflen + copylen) > deflate_zbufsz) {
          size_t new_bufsz;
          Byte *tmp;

          new_bufsz = deflate_zbufsz;
          while ((deflate_zbuflen + copylen) > new_bufsz) {
            pr_signals_handle();
            new_bufsz *= 2;
          }

          pr_trace_msg(trace_channel, 9,
            "read: allocated new deflate buffer (size %lu)",
            (unsigned long) new_bufsz);

          tmp = palloc(session.pool, new_bufsz);
          memcpy(tmp, deflate_zbuf, deflate_zbuflen);

          deflate_zbuf_ptr = deflate_zbuf = tmp;
          deflate_zbufsz = new_bufsz;
        } 

        break;

      default:
        pr_trace_msg(trace_channel, 3,
          "read: error inflating %lu bytes of data: [%d] %s: %s",
          (unsigned long) datalen, deflate_zerrno,
          deflate_zstrerror(deflate_zerrno),
          zstrm->msg ? zstrm->msg : "unavailable");

        errno = xerrno;
        (void) pr_log_writefile(deflate_logfd, MOD_DEFLATE_VERSION,
          "error inflating %lu bytes of data: [%d] %s",
          (unsigned long) datalen, deflate_zerrno,
          zstrm->msg ? zstrm->msg : deflate_zstrerror(deflate_zerrno));

        errno = EIO;
        return -1;
    }

    deflate_zbuflen = deflate_zbufsz - zstrm->avail_out;

    /* Now all we have to do is return EAGAIN, so that the FSIO API calls
     * us back immediately.  That will hit the check for deflate_zbuflen
     * earlier in this function, and return the data we just decompressed.
     */
    errno = EAGAIN;
    return -1;
  }

  return read(nstrm->strm_fd, buf, bufsz);
}

static int deflate_netio_shutdown_cb(pr_netio_stream_t *nstrm, int how) {

  if (nstrm->strm_type == PR_NETIO_STRM_DATA) {
    int res = 0;
    z_stream *zstrm = nstrm->strm_data;

    if (nstrm->strm_mode == PR_NETIO_IO_WR) {
      zstrm->next_in = Z_NULL;
      zstrm->avail_in = 0;

      pr_trace_msg(trace_channel, 19,
        "shutdown: pre-deflate zstream state: avail_in = %d, avail_out = %d",
        zstrm->avail_in, zstrm->avail_out);

      deflate_zerrno = deflate(zstrm, Z_FINISH);

      pr_trace_msg(trace_channel, 19,
        "shutdown: post-inflate zstream state: avail_in = %d, avail_out = %d "
        "(zerrno = %s)", zstrm->avail_in, zstrm->avail_out,
        deflate_zstrerror(deflate_zerrno));

      if (deflate_zerrno != Z_OK &&
          deflate_zerrno != Z_STREAM_END) {
        pr_trace_msg(trace_channel, 3,
          "shutdown: error deflating data: [%d] %s: %s", deflate_zerrno,
          deflate_zstrerror(deflate_zerrno),
          zstrm->msg ? zstrm->msg : "unavailable");

        (void) pr_log_writefile(deflate_logfd, MOD_DEFLATE_VERSION,
          "error deflating data: [%d] %s", deflate_zerrno,
          zstrm->msg ? zstrm->msg : deflate_zstrerror(deflate_zerrno));

      } else {
        size_t datalen, offset;

        datalen = deflate_zbufsz - zstrm->avail_out;
        offset = 0;

        while (datalen > 0) {
          res = write(nstrm->strm_fd, deflate_zbuf + offset, datalen);
          if (res < 0) {
            if (errno == EINTR ||
                errno == EAGAIN) {
              /* The socket might be busy, especially if the peer is a bit
               * slow in reading data from it.
               */
              pr_signals_handle();
              continue;
            }

            (void) pr_log_writefile(deflate_logfd, MOD_DEFLATE_VERSION,
              "error writing to socket %d: %s", nstrm->strm_fd,
              strerror(errno));
            return -1;
          }

          /* Manually update the "raw" bytes counter, so that it will be
           * accurate for %O logging.
           */
          session.total_raw_out += res;

          /* Watch out for short writes. */
          if (res == datalen) {
            break;
          }

          offset += res;
          datalen -= res;
        }
      }

      return 0;
    }
  }

  return shutdown(nstrm->strm_fd, how);
}

static int deflate_netio_write_cb(pr_netio_stream_t *nstrm, char *buf,
    size_t buflen) {

  if (buflen == 0) {
    return 0;
  }

  if (nstrm->strm_type == PR_NETIO_STRM_DATA) {
    int res = 0, xerrno;
    size_t datalen, offset = 0;
    z_stream *zstrm = nstrm->strm_data;

    /* Deflate the data to be written out. */
    zstrm->next_in = (Bytef *) buf;
    zstrm->avail_in = buflen;

    pr_trace_msg(trace_channel, 19,
      "write: pre-deflate zstream state: avail_in = %d, avail_out = %d",
      zstrm->avail_in, zstrm->avail_out);

    deflate_zerrno = deflate(zstrm, Z_SYNC_FLUSH);
    xerrno = errno;

    pr_trace_msg(trace_channel, 19,
      "write: post-inflate zstream state: avail_in = %d, avail_out = %d "
      "(zerrno = %s)", zstrm->avail_in, zstrm->avail_out,
      deflate_zstrerror(deflate_zerrno));

    errno = xerrno;

    if (deflate_zerrno != Z_OK) {
      pr_trace_msg(trace_channel, 3, "write: error deflating data: [%d] %s: %s",
        deflate_zerrno, deflate_zstrerror(deflate_zerrno),
        zstrm->msg ? zstrm->msg : "unavailable");

      errno = xerrno;

      (void) pr_log_writefile(deflate_logfd, MOD_DEFLATE_VERSION,
        "error deflating data: [%d] %s", deflate_zerrno,
        zstrm->msg ? zstrm->msg : deflate_zstrerror(deflate_zerrno));

      errno = EIO;
      return -1;
    }

    datalen = deflate_zbufsz - zstrm->avail_out;

    while (datalen > 0) {
      pr_signals_handle();

      res = write(nstrm->strm_fd, deflate_zbuf + offset, datalen);
      if (res < 0) {
        if (errno == EINTR ||
            errno == EAGAIN) {
          /* The socket might be busy, especially if the peer is a bit
           * slow in reading data from it.
           */
          pr_signals_handle();
          continue;
        }

        (void) pr_log_writefile(deflate_logfd, MOD_DEFLATE_VERSION,
          "error writing to socket %d: %s", nstrm->strm_fd, strerror(errno));
        return -1;
      }

      /* Manually adjust the "raw" bytes counter, so that it will be
       * accurate for %O logging.
       */
      session.total_raw_out += res;

      (void) pr_log_writefile(deflate_logfd, MOD_DEFLATE_VERSION,
        "wrote %d (of %lu) bytes of compressed of data to socket %d", res,
        (unsigned long) datalen, nstrm->strm_fd);

      /* Watch out for short writes */
      if (res == datalen) {
        zstrm->next_out = deflate_zbuf;
        zstrm->avail_out = deflate_zbufsz;
        break;

      } else {
        offset += res;
        datalen -= res;
      }
    }

    /* Manually adjust the "raw" bytes in counter, so that it will
     * be accurate for %O logging.
     *
     * We subtract the number we are returning here, since our return
     * value will simply be added back to the counter in pr_netio_write().
     * And if our subtraction causes an underflow, it's still OK since
     * the subsequent addition will overflow, and get the value back to
     * what it should be.
     */

    res = (buflen - zstrm->avail_in);
    session.total_raw_out -= res;

    pr_trace_msg(trace_channel, 9, "write: returning %d for %lu bytes",
      res, (unsigned long) buflen);
    return res;
  }

  return write(nstrm->strm_fd, buf, buflen);
}

/* Configuration handlers
 */

/* usage: DeflateEngine on|off */
MODRET set_deflateengine(cmd_rec *cmd) {
  int bool;
  config_rec *c;

  CHECK_ARGS(cmd, 1);
  CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);

  bool = get_boolean(cmd, 1);
  if (bool == -1)
    CONF_ERROR(cmd, "expected Boolean parameter");

  c = add_config_param(cmd->argv[0], 1, NULL);
  c->argv[0] = pcalloc(c->pool, sizeof(unsigned int));
  *((unsigned int *) c->argv[0]) = bool;

  return PR_HANDLED(cmd);
}

/* usage: DeflateLog path|"none" */
MODRET set_deflatelog(cmd_rec *cmd) {
  CHECK_ARGS(cmd, 1);
  CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);

  if (pr_fs_valid_path(cmd->argv[1]) < 0)
    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, ": ", cmd->argv[1],
      " is not a valid path", NULL));

  add_config_param_str(cmd->argv[0], 1, cmd->argv[1]);
  return PR_HANDLED(cmd);
}

/* Command handlers
 */

MODRET deflate_opts(cmd_rec *cmd) {
  if (!deflate_engine)
    return PR_DECLINED(cmd);

  if (cmd->argc < 3) {
    return PR_DECLINED(cmd);
  }

  if (strcasecmp(cmd->argv[1], "Z") == 0) {

    /* Twiddle the requested ZLIB parameters. */

    if (cmd->argc == 3) {
      /* If no key/value pairs were given, reset the deflate parameters
       * to their default settings.
       */
      deflate_compression_level = MOD_DEFLATE_DEFAULT_COMPRESS_LEVEL;
      deflate_mem_level = MOD_DEFLATE_DEFAULT_MEM_LEVEL;
      deflate_strategy = MOD_DEFLATE_DEFAULT_STRATEGY;
      deflate_window_bits = MOD_DEFLATE_DEFAULT_WINDOW_BITS;

      pr_response_add(R_200, _("%s OK"), cmd->argv[0]);
      return PR_HANDLED(cmd);

    } else {
      register unsigned int i;

      if (cmd->argc % 2 != 0) {
        pr_response_add_err(R_501, _("Bad number of parameters"));
        return PR_ERROR(cmd);
      }

      for (i = 2; i < cmd->argc; i += 2) {
        if (strcasecmp(cmd->argv[i], "blocksize") == 0 ||
            strcasecmp(cmd->argv[i], "engine") == 0) {
          pr_response_add_err(R_501, _("%s: unsupported MODE Z option: %s"),
            cmd->argv[0], cmd->argv[i]);
          return PR_ERROR(cmd);

        } else if (strcasecmp(cmd->argv[i], "level") == 0) {
          int level = atoi(cmd->argv[i+1]);

          if (level < 0 ||
              level > 9) {
            pr_response_add_err(R_501, _("%s: bad MODE Z option value: %s %s"),
              cmd->argv[0], cmd->argv[i], cmd->argv[i+1]);
            return PR_ERROR(cmd);
          }

          deflate_compression_level = level;

        } else {
          pr_response_add_err(R_501, _("%s: unknown MODE Z option: %s"),
            cmd->argv[0], cmd->argv[i]);
          return PR_ERROR(cmd);
        }
      }
    }

    pr_response_add(R_200, _("OPTS MODE Z OK"));
    return PR_HANDLED(cmd);
  }

  return PR_DECLINED(cmd);
}

MODRET deflate_mode(cmd_rec *cmd) {
  if (!deflate_engine) {
    return PR_DECLINED(cmd);
  }

  if (cmd->argc != 2) {
    (void) pr_log_writefile(deflate_logfd, MOD_DEFLATE_VERSION,
      "declining MODE Z (wrong number of arguments: %d)", cmd->argc);
    return PR_DECLINED(cmd);
  }

  cmd->argv[1][0] = toupper(cmd->argv[1][0]);

  if (cmd->argv[1][0] == 'Z') {
    if (session.rfc2228_mech) {
      (void) pr_log_writefile(deflate_logfd, MOD_DEFLATE_VERSION,
        "declining MODE Z (RFC2228 mechanism '%s' in effect)",
        session.rfc2228_mech);
      pr_log_debug(DEBUG2, MOD_DEFLATE_VERSION
        ": declining MODE Z (RFC2228 mechanism '%s' in effect)",
        session.rfc2228_mech);

      pr_response_add_err(R_504, _("Unable to handle MODE Z at this time"));
      return PR_ERROR(cmd);
    }

    if (deflate_enabled) {
      pr_response_add(R_200, _("OK"));
      return PR_HANDLED(cmd);
    }

    /* Need to install some sort of NetIO handlers here, to handle
     * compression.
     */

    deflate_netio = pr_alloc_netio(session.pool);
    deflate_netio->close = deflate_netio_close_cb;
    deflate_netio->open = deflate_netio_open_cb;
    deflate_netio->read = deflate_netio_read_cb;
    deflate_netio->shutdown = deflate_netio_shutdown_cb;
    deflate_netio->write = deflate_netio_write_cb;

    /* XXX Report on any previously registered NetIO streams (e.g. mod_tls)?
     * Doing so would require a new pr_netio_ function, e.g. pr_netio_get().
     */
    pr_unregister_netio(PR_NETIO_STRM_DATA);

    if (pr_register_netio(deflate_netio, PR_NETIO_STRM_DATA) < 0) {
      (void) pr_log_writefile(deflate_logfd, MOD_DEFLATE_VERSION,
        "error registering netio: %s", strerror(errno));

    } else {
      deflate_enabled = TRUE;

      pr_response_add(R_200, _("OK"));
      return PR_HANDLED(cmd);
    }

  } else {
    if (deflate_enabled) {
      /* Switch to some other transmission mode.  Remove our NetIO.
       * XXX Ideally, we would restore the previous NetIO.
       */

      if (pr_unregister_netio(PR_NETIO_STRM_DATA) < 0) {
        (void) pr_log_writefile(deflate_logfd, MOD_DEFLATE_VERSION,
          "error unregistering netio: %s", strerror(errno));

      } else {
        (void) pr_log_writefile(deflate_logfd, MOD_DEFLATE_VERSION,
          "%s %s: unregistered netio", cmd->argv[0], cmd->argv[1]);
      }

      if (deflate_netio) {
        destroy_pool(deflate_netio->pool);
        deflate_netio = NULL;
      }

      deflate_enabled = FALSE;
    }
  }

  return PR_DECLINED(cmd);
}

/* Initialization functions
 */

static int deflate_sess_init(void) {
  config_rec *c;

  c = find_config(main_server->conf, CONF_PARAM, "DeflateEngine", FALSE);
  if (c &&
      *((unsigned int *) c->argv[0]) == TRUE) {
    deflate_engine = TRUE;

  } else {
    return 0;
  }

  /* Add a FEAT string indicating support of MODE Z.  Note that, in the
   * future, other compression engines (e.g. bzip2) will need to be handled
   * here as well.
   */
  pr_feat_add("MODE Z");

  c = find_config(main_server->conf, CONF_PARAM, "DeflateLog", FALSE);
  if (c &&
      strcasecmp(c->argv[0], "none") != 0) {
    int res;

    pr_signals_block();
    PRIVS_ROOT
    res = pr_log_openfile(c->argv[0], &deflate_logfd, 0640);
    PRIVS_RELINQUISH
    pr_signals_unblock();

    switch (res) {
      case -1:
        pr_log_pri(PR_LOG_NOTICE, MOD_DEFLATE_VERSION
          ": notice: unable to open DeflateLog '%s': %s",
          (char *) c->argv[0], strerror(errno));
        break;

      case PR_LOG_WRITABLE_DIR:
        pr_log_pri(PR_LOG_NOTICE, MOD_DEFLATE_VERSION
          ": notice: unable to use DeflateLog '%s': parent directory is "
            "world-writeable", (char *) c->argv[0]);
        break;

      case PR_LOG_SYMLINK:
        pr_log_pri(PR_LOG_NOTICE, MOD_DEFLATE_VERSION
          ": notice: unable to use DeflateLog '%s': cannot log to a symlink",
          (char *) c->argv[0]);
        break;
    }
  }

  /* Allocate the buffers which will be used for inflating/deflating data.
   * Look up the optimal transfer buffer size, and use a factor of 8.
   * Later, if needed, a larger buffer will be allocated when necessary.
   */
  deflate_zbufsz = pr_config_get_xfer_bufsz() * 8;
  deflate_zbuf_ptr = deflate_zbuf = pcalloc(session.pool, deflate_zbufsz);
  deflate_zbuflen = 0;

  deflate_rbufsz = pr_config_get_xfer_bufsz();
  deflate_rbuf = palloc(session.pool, deflate_rbufsz);
  deflate_rbuflen = 0;

  return 0;
}

static int deflate_init(void) {
  pr_log_debug(DEBUG5, MOD_DEFLATE_VERSION ": using zlib " ZLIB_VERSION);
  return 0;
}

/* Module API tables
 */

static conftable deflate_conftab[] = {
  { "DeflateEngine",		set_deflateengine,		NULL },
  { "DeflateLog",		set_deflatelog,			NULL },
  { NULL }
};

static cmdtable deflate_cmdtab[] = {
  { CMD, C_OPTS "_MODE",	G_NONE, deflate_opts,	FALSE, FALSE, CL_MISC },
  { CMD, C_MODE,		G_NONE, deflate_mode,	FALSE, FALSE, CL_MISC },
  { 0, NULL }
};

module deflate_module = {
  NULL, NULL,

  /* Module API version 2.0 */
  0x20,

  /* Module name */
  "deflate",

  /* Module configuration handler table */
  deflate_conftab,

  /* Module command handler table */
  deflate_cmdtab,

  /* Module authentication handler table */
  NULL,

  /* Module initialization function */
  deflate_init,

  /* Session initialization function */
  deflate_sess_init,

  /* Module version */
  MOD_DEFLATE_VERSION
};
