[Git][debian-proftpd-team/proftpd-mod-autohost][upstream] New upstream version 0.6

Hilmar Preuße gitlab at salsa.debian.org
Tue Aug 18 08:26:13 BST 2020



Hilmar Preuße pushed to branch upstream at Debian ProFTPD Team / proftpd-mod-autohost


Commits:
eb22f567 by Hilmar Preuße at 2020-08-18T09:19:18+02:00
New upstream version 0.6
- - - - -


11 changed files:

- + .gitattributes
- + .gitignore
- + .travis.yml
- + README.md
- mod_autohost.c
- mod_autohost.html
- t/lib/ProFTPD/Tests/Modules/mod_autohost.pm
- + t/lib/ProFTPD/Tests/Modules/mod_autohost/host.pm
- + t/lib/ProFTPD/Tests/Modules/mod_autohost/sni.pm
- + t/modules/mod_autohost/host.t
- + t/modules/mod_autohost/sni.t


Changes:

=====================================
.gitattributes
=====================================
@@ -0,0 +1,2 @@
+*.pl linguist-language=C
+*.pm linguist-language=C


=====================================
.gitignore
=====================================
@@ -0,0 +1,2 @@
+*.log
+*.sw?


=====================================
.travis.yml
=====================================
@@ -0,0 +1,33 @@
+env: TRAVIS_CI=true
+language: c
+
+compiler:
+  - gcc
+  - clang
+
+install:
+  # for unit tests
+  - sudo apt-get install -y check
+  - sudo apt-get install -y libsubunit-dev
+  # for static code analysis
+  # - sudo apt-get install -y cppcheck
+  # - sudo apt-get install -y rats
+  # for test code coverage
+  - sudo apt-get install -y lcov
+  - gem install coveralls-lcov
+
+before_script:
+  - cd ${TRAVIS_BUILD_DIR}
+  - lcov --directory . --zerocounters
+
+script:
+  # - find . -type f -name "*.c" -print | grep -v t\/ | xargs cppcheck 2>&1
+  # - find . -type f -name "*.c" -print | grep -v t\/ | xargs rats --language=c
+  - git clone --depth 100 https://github.com/proftpd/proftpd.git
+  - cp mod_autohost.c proftpd/contrib/
+  - cd proftpd/
+  - ./configure LIBS="-lm -lsubunit -lrt -pthread" --enable-devel=coverage --enable-dso --enable-tests --with-shared=mod_autohost
+  - make
+  - make clean
+  - ./configure LIBS="-lm -lsubunit -lrt -pthread" --enable-devel=coverage --enable-tests --with-modules=mod_autohost
+  - make


=====================================
README.md
=====================================
@@ -0,0 +1,13 @@
+proftpd-mod_autohost
+====================
+
+Status
+------
+[![Build Status](https://travis-ci.org/Castaglia/proftpd-mod_autohost.svg?branch=master)](https://travis-ci.org/Castaglia/proftpd-mod_autohost)
+
+Synopsis
+--------
+The `mod_autohost` module for ProFTPD is used to support large numbers
+of `<VirtualHost>` sections _without_ requiring large config files to do so.
+
+For further module documentation, see [mod_autohost.html](https://htmlpreview.github.io/?https://github.com/Castaglia/proftpd-mod_autohost/blob/master/mod_autohost.html).


=====================================
mod_autohost.c
=====================================
@@ -1,7 +1,6 @@
 /*
  * ProFTPD: mod_autohost -- a module for mass virtual hosting
- *
- * Copyright (c) 2004-2011 TJ Saunders
+ * Copyright (c) 2004-2020 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
@@ -24,89 +23,145 @@
  *
  * This is mod_autohost, contrib software for proftpd 1.3.x and above.
  * For more information contact TJ Saunders <tj at castaglia.org>.
- *
- * $Id: mod_autohost.c,v 1.4 2011/03/01 21:29:09 tj Exp tj $
  */
 
 #include "conf.h"
 #include "privs.h"
 
-#define MOD_AUTOHOST_VERSION		"mod_autohost/0.4"
+#define MOD_AUTOHOST_VERSION		"mod_autohost/0.6"
 
-#if PROFTPD_VERSION_NUMBER < 0x0001030401
-# error "ProFTPD 1.3.4rc1 or later required"
+#if PROFTPD_VERSION_NUMBER < 0x0001030604
+# error "ProFTPD 1.3.6 or later required"
 #endif
 
 module autohost_module;
 
 static const char *autohost_config = NULL;
-static unsigned int autohost_engine = FALSE;
+static int autohost_engine = FALSE;
 static int autohost_logfd = -1;
 static pool *autohost_pool = NULL;
 static xaset_t *autohost_server_list = NULL;
 
-/* XXX Note: this function makes crass assumptions about IPv4 connections;
- * we are soon going to need to properly support IPv6 addresses/connections.
- */
-static char *autohost_get_config(conn_t *conn) {
+static const char *trace_channel = "autohost";
+
+static char *autohost_get_config(conn_t *conn, const char *server_name) {
   char *ipstr, *portstr, *path = (char *) autohost_config;
-  char *oct1str, *oct2str, *oct3str, *oct4str;
-  char *start, *end;
+  int family;
 
+  family = pr_netaddr_get_family(conn->local_addr);
   ipstr = (char *) pr_netaddr_get_ipstr(conn->local_addr);
 
-  start = ipstr;
-  end = strchr(start, '.');
-  *end = '\0';
-  oct1str = pstrdup(autohost_pool, start);
+  if (family == AF_INET) {
+    char *oct1str, *oct2str, *oct3str, *oct4str;
+    char *start, *end;
+
+    start = ipstr;
+    end = strchr(start, '.');
+    *end = '\0';
+    oct1str = pstrdup(autohost_pool, start);
+
+    start = end + 1;
+    *end = '.';
+    end = strchr(start, '.');
+    *end = '\0';
+    oct2str = pstrdup(autohost_pool, start);
+
+    start = end + 1;
+    *end = '.';
+    end = strchr(start, '.');
+    *end = '\0';
+    oct3str = pstrdup(autohost_pool, start);
+
+    start = end + 1;
+    *end = '.';
+    oct4str = pstrdup(autohost_pool, start);
+
+    if (strstr(path, "%1") != NULL) {
+      path = (char *) sreplace(autohost_pool, path, "%1", oct1str, NULL);
+    }
 
-  start = end + 1;
-  *end = '.';
-  end = strchr(start, '.');
-  *end = '\0';
-  oct2str = pstrdup(autohost_pool, start);
+    if (strstr(path, "%2") != NULL) {
+      path = (char *) sreplace(autohost_pool, path, "%2", oct2str, NULL);
+    }
 
-  start = end + 1;
-  *end = '.';
-  end = strchr(start, '.');
-  *end = '\0';
-  oct3str = pstrdup(autohost_pool, start);
+    if (strstr(path, "%3") != NULL) {
+      path = (char *) sreplace(autohost_pool, path, "%3", oct3str, NULL);
+    }
 
-  start = end + 1;
-  *end = '.';
-  oct4str = pstrdup(autohost_pool, start);
+    if (strstr(path, "%4") != NULL) {
+      path = (char *) sreplace(autohost_pool, path, "%4", oct4str, NULL);
+    }
+  }
 
   portstr = pcalloc(autohost_pool, 10);
   snprintf(portstr, 10, "%u", conn->local_port);
 
   if (strstr(path, "%0") != NULL) {
-    path = sreplace(autohost_pool, path, "%0", ipstr, NULL);
+    path = (char *) sreplace(autohost_pool, path, "%0", ipstr, NULL);
   }
 
-  if (strstr(path, "%1") != NULL) {
-    path = sreplace(autohost_pool, path, "%1", oct1str, NULL);
+  if (server_name != NULL) {
+    /* Note: How to handle the case-insensitive name of SNI/HOST, and the
+     * case-sensitive nature of paths.  Should we always downcase the entire
+     * server name, for purposes of config file lookup?
+     */
+    if (strstr(path, "%n") != NULL) {
+      path = (char *) sreplace(autohost_pool, path, "%n", server_name, NULL);
+    }
   }
 
-  if (strstr(path, "%2") != NULL) {
-    path = sreplace(autohost_pool, path, "%2", oct2str, NULL);
+  if (strstr(path, "%p") != NULL) {
+    path = (char *) sreplace(autohost_pool, path, "%p", portstr, NULL);
   }
 
-  if (strstr(path, "%3") != NULL) {
-    path = sreplace(autohost_pool, path, "%3", oct3str, NULL);
-  }
+  return path;
+}
 
-  if (strstr(path, "%4") != NULL) {
-    path = sreplace(autohost_pool, path, "%4", oct4str, NULL);
+/* Largely borrowed/copied from src/bindings.c. */
+static unsigned int process_serveralias(server_rec *s) {
+  unsigned namebind_count = 0;
+  config_rec *c;
+
+  /* If there is no ipbind already for this server, we cannot associate
+   * any ServerAlias-based namebinds to it.
+   */
+  if (pr_ipbind_get_server(s->addr, s->ServerPort) == NULL) {
+    return 0;
   }
 
-  if (strstr(path, "%p") != NULL) {
-    path = sreplace(autohost_pool, path, "%p", portstr, NULL);
+  c = find_config(s->conf, CONF_PARAM, "ServerAlias", FALSE);
+  while (c != NULL) {
+    int res;
+
+    pr_signals_handle();
+
+    res = pr_namebind_create(s, c->argv[0], s->addr, s->ServerPort);
+    if (res == 0) {
+      namebind_count++;
+
+      res = pr_namebind_open(c->argv[0], s->addr, s->ServerPort);
+      if (res < 0) {
+        pr_trace_msg(trace_channel, 2,
+          "notice: unable to open namebind '%s': %s", (char *) c->argv[0],
+          strerror(errno));
+      }
+
+    } else {
+      if (errno != ENOENT) {
+        pr_trace_msg(trace_channel, 3,
+          "unable to create namebind for '%s' to %s#%u: %s",
+          (char *) c->argv[0], pr_netaddr_get_ipstr(s->addr), s->ServerPort,
+          strerror(errno));
+      }
+    }
+
+    c = find_config_next(c, c->next, CONF_PARAM, "ServerAlias", FALSE);
   }
 
-  return path;
+  return namebind_count;
 }
 
-static int autohost_parse_config(conn_t *conn, char *path) {
+static int autohost_parse_config(conn_t *conn, const char *path) {
   server_rec *s;
   pr_ipbind_t *binding;
 
@@ -131,8 +186,12 @@ static int autohost_parse_config(conn_t *conn, char *path) {
   pr_parser_cleanup();
 
   if (fixup_servers(autohost_server_list) < 0) {
+    int xerrno = errno;
+
     (void) pr_log_writefile(autohost_logfd, MOD_AUTOHOST_VERSION,
-      "error fixing up autohost: %s", strerror(errno));
+      "error fixing up autohost: %s", strerror(xerrno));
+
+    errno = xerrno;
     return -1;
   }
 
@@ -142,30 +201,39 @@ static int autohost_parse_config(conn_t *conn, char *path) {
   /* Now that we have a valid server_rec, we need to bind it to
    * the address to which the client connected.
    */
+  process_serveralias(s);
 
   binding = pr_ipbind_find(conn->local_addr, conn->local_port, TRUE);
-  if (binding == NULL) {
-    if (pr_ipbind_create(s, conn->local_addr, conn->local_port) < 0) {
-      (void) pr_log_writefile(autohost_logfd, MOD_AUTOHOST_VERSION,
-        "error creating binding: %s", strerror(errno));
-      return -1;
-    }
-
-    if (pr_ipbind_open(conn->local_addr, conn->local_port, main_server->listen,
-        TRUE, TRUE, FALSE) < 0) {
-      (void) pr_log_writefile(autohost_logfd, MOD_AUTOHOST_VERSION,
-        "error opening binding for %s#%d: %s",
-        pr_netaddr_get_ipstr(conn->local_addr), conn->local_port,
-          strerror(errno));
-      return -1;
-    }
-
-  } else {
-
+  if (binding != NULL) {
     /* If we already have a binding in place, we need to replace the
      * server_rec to which that binding points with our new server_rec.
      */
     binding->ib_server = s;
+
+    return 0;
+  }
+
+  if (pr_ipbind_create(s, conn->local_addr, conn->local_port) < 0) {
+    int xerrno = errno;
+
+    (void) pr_log_writefile(autohost_logfd, MOD_AUTOHOST_VERSION,
+      "error creating binding: %s", strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  if (pr_ipbind_open(conn->local_addr, conn->local_port, main_server->listen,
+      TRUE, TRUE, FALSE) < 0) {
+    int xerrno = errno;
+
+    (void) pr_log_writefile(autohost_logfd, MOD_AUTOHOST_VERSION,
+      "error opening binding for %s#%d: %s",
+      pr_netaddr_get_ipstr(conn->local_addr), conn->local_port,
+      strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
   }
 
   return 0;
@@ -190,19 +258,20 @@ MODRET set_autohostconfig(cmd_rec *cmd) {
 
 /* usage: AutoHostEngine on|off */
 MODRET set_autohostengine(cmd_rec *cmd) {
-  int bool;
+  int engine;
   config_rec *c;
 
   CHECK_ARGS(cmd, 1);
   CHECK_CONF(cmd, CONF_ROOT);
 
-  bool = get_boolean(cmd, 1);
-  if (bool == -1)
+  engine = get_boolean(cmd, 1);
+  if (engine == -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;
+  c->argv[0] = pcalloc(c->pool, sizeof(int));
+  *((int *) c->argv[0]) = engine;
 
   return PR_HANDLED(cmd);
 }
@@ -255,25 +324,52 @@ MODRET set_autohostports(cmd_rec *cmd) {
   return PR_HANDLED(cmd);
 }
 
-/* Event handlers
+/* Command handlers
+ */
+
+MODRET autohost_pre_host(cmd_rec *cmd) {
+  const char *path, *server_name;
+  struct stat st;
+
+  if (autohost_engine == FALSE) {
+    return PR_DECLINED(cmd);
+  }
+
+  server_name = (const char *) cmd->argv[1];
+  path = autohost_get_config(session.c, server_name);
+  pr_trace_msg(trace_channel, 4, "using AutoHostConfig path '%s' for '%s %s'",
+    path, (const char *) cmd->argv[0], server_name);
+
+  if (pr_fsio_stat(path, &st) < 0) {
+    (void) pr_log_writefile(autohost_logfd, MOD_AUTOHOST_VERSION,
+      "error checking for '%s': %s", path, strerror(errno));
+    return PR_DECLINED(cmd);
+  }
+
+  if (autohost_parse_config(session.c, path) < 0) {
+    (void) pr_log_writefile(autohost_logfd, MOD_AUTOHOST_VERSION,
+      "error parsing '%s': %s", path, strerror(errno));
+    return PR_DECLINED(cmd);
+  }
+
+  pr_trace_msg(trace_channel, 9, "'%s %s' found using autohost for %s#%u",
+    (const char *) cmd->argv[0], server_name,
+    pr_netaddr_get_ipstr(session.c->local_addr), session.c->local_port);
+
+  return PR_DECLINED(cmd);
+}
+
+/* Event listeners
  */
 
 static void autohost_connect_ev(const void *event_data, void *user_data) {
-  char *path;
+  const char *path;
   struct stat st;
   conn_t *conn = (conn_t *) event_data;
  
-  if (!autohost_engine)
-    return;
-
-#ifdef PR_USE_IPV6
-  /* NOTE: we currently do not handle IPv6 address. */
-  if (pr_netaddr_get_family(conn->local_addr) == AF_INET6) {
-    pr_log_debug(DEBUG0, MOD_AUTOHOST_VERSION
-      ": unable to handle IPv6 addresses");
+  if (autohost_engine == FALSE) {
     return;
   }
-#endif /* PR_USE_IPV6 */
 
   /* Autohost config files, if found, will take precedence over a matching
    * server config found in the main config file.
@@ -291,11 +387,26 @@ static void autohost_connect_ev(const void *event_data, void *user_data) {
    * It is allocated after the fork().
    */
 
-  path = autohost_get_config(conn);  
+  path = autohost_get_config(conn, NULL);
+  pr_trace_msg(trace_channel, 4, "using AutoHostConfig path '%s'", path);
 
   if (pr_fsio_stat(path, &st) < 0) {
-    (void) pr_log_writefile(autohost_logfd, MOD_AUTOHOST_VERSION,
-      "error checking for '%s': %s", path, strerror(errno));
+    int xerrno = errno;
+
+    /* If the given path contains '%n', and the path is not found, do not log
+     * the error.  The file in question may be intended for name-based vhosts,
+     * which can only be resolved at SNI/HOST time, not at TCP connect time.
+     */
+    if (xerrno == ENOENT &&
+        strstr(path, "%n") != NULL) {
+      pr_trace_msg(trace_channel, 19,
+        "ignoring connect-time check of name-based config file '%s'", path);
+
+    } else {
+      (void) pr_log_writefile(autohost_logfd, MOD_AUTOHOST_VERSION,
+        "error checking for '%s': %s", path, strerror(xerrno));
+    }
+
     return;
   }
 
@@ -305,50 +416,91 @@ static void autohost_connect_ev(const void *event_data, void *user_data) {
     return;
   }
 
-  (void) pr_log_writefile(autohost_logfd, MOD_AUTOHOST_VERSION,
-    "found autohost for %s#%u", pr_netaddr_get_ipstr(conn->local_addr),
-    conn->local_port);
+  pr_trace_msg(trace_channel, 9, "found using autohost for %s#%u",
+    pr_netaddr_get_ipstr(conn->local_addr), conn->local_port);
 
   return;
 }
 
 #if defined(PR_SHARED_MODULE)
 static void autohost_mod_unload_ev(const void *event_data, void *user_data) {
-  if (strcmp("mod_autohost.c", (const char *) event_data) == 0) {
-    pr_event_unregister(&autohost_module, NULL, NULL);
+  if (strcmp("mod_autohost.c", (const char *) event_data) != 0) {
+    return;
+  }
+
+  pr_event_unregister(&autohost_module, NULL, NULL);
+
+  if (autohost_pool != NULL) {
+    destroy_pool(autohost_pool);
+    autohost_pool = NULL;
   }
 }
-#endif
+#endif /* PR_SHARED_MODULE */
+
+static void autohost_sni_ev(const void *event_data, void *user_data) {
+  const char *path, *server_name;
+  struct stat st;
+
+  if (autohost_engine == FALSE) {
+    return;
+  }
+
+  server_name = (const char *) event_data;
+  path = autohost_get_config(session.c, server_name);
+  pr_trace_msg(trace_channel, 4,
+    "using AutoHostConfig path '%s' for TLS SNI '%s'", path, server_name);
+
+  if (pr_fsio_stat(path, &st) < 0) {
+    (void) pr_log_writefile(autohost_logfd, MOD_AUTOHOST_VERSION,
+      "error checking for '%s': %s", path, strerror(errno));
+    return;
+  }
+
+  if (autohost_parse_config(session.c, path) < 0) {
+    (void) pr_log_writefile(autohost_logfd, MOD_AUTOHOST_VERSION,
+      "error parsing '%s': %s", path, strerror(errno));
+    return;
+  }
+
+  pr_trace_msg(trace_channel, 9, "TLS SNI '%s' found using autohost for %s#%u",
+    server_name, pr_netaddr_get_ipstr(session.c->local_addr),
+    session.c->local_port);
+
+  return;
+}
 
 static void autohost_postparse_ev(const void *event_data, void *user_data) {
   config_rec *c;
 
   c = find_config(main_server->conf, CONF_PARAM, "AutoHostEngine", FALSE);
-  if (c) {
-    autohost_engine = *((unsigned int *) c->argv[0]);
+  if (c != NULL) {
+    autohost_engine = *((int *) c->argv[0]);
   }
 
-  if (!autohost_engine)
+  if (autohost_engine == FALSE) {
     return;
+  }
 
   autohost_pool = make_sub_pool(permanent_pool);
   pr_pool_tag(autohost_pool, MOD_AUTOHOST_VERSION);
 
   pr_event_register(&autohost_module, "core.connect", autohost_connect_ev,
     NULL);
+  pr_event_register(&autohost_module, "mod_tls.sni", autohost_sni_ev, NULL);
 
   c = find_config(main_server->conf, CONF_PARAM, "AutoHostConfig", FALSE);
-  if (c) {
+  if (c != NULL) {
     autohost_config = c->argv[0];
 
   } else {
     pr_log_debug(DEBUG0, MOD_AUTOHOST_VERSION
       ": missing required AutoHostConfig");
-    end_login(1);
+    pr_session_disconnect(&autohost_module, PR_SESS_DISCONNECT_BAD_CONFIG,
+      "missing required AutoHostConfig directive");
   }
 
   c = find_config(main_server->conf, CONF_PARAM, "AutoHostLog", FALSE);
-  if (c) {
+  if (c != NULL) {
     int res;
     char *autohost_log;
 
@@ -385,7 +537,7 @@ static void autohost_postparse_ev(const void *event_data, void *user_data) {
   autohost_server_list = xaset_create(autohost_pool, NULL);
 
   c = find_config(main_server->conf, CONF_PARAM, "AutoHostPorts", FALSE);
-  if (c) {
+  if (c != NULL) {
     register unsigned int i;
     array_header *port_list;
     int *ports;
@@ -468,6 +620,13 @@ static conftable autohost_conftab[] = {
   { NULL }
 };
 
+static cmdtable autohost_cmdtab[] = {
+#if defined(C_HOST)
+  { PRE_CMD,	C_HOST,	G_NONE,	autohost_pre_host,	FALSE,	FALSE },
+#endif /* FTP HOST support */
+  { 0, NULL }
+};
+
 module autohost_module = {
   NULL, NULL,
 
@@ -481,7 +640,7 @@ module autohost_module = {
   autohost_conftab,
 
   /* Module command handler table */
-  NULL,
+  autohost_cmdtab,
 
   /* Module authentication handler table */
   NULL,


=====================================
mod_autohost.html
=====================================
@@ -1,6 +1,3 @@
-<!-- $Id: mod_autohost.html,v 1.5 2009/03/03 19:00:07 tj Exp tj $ -->
-<!-- $Source: /home/tj/proftpd/modules/doc/RCS/mod_autohost.html,v $ -->
-
 <html>
 <head>
 <title>ProFTPD module mod_autohost</title>
@@ -15,26 +12,26 @@
 <hr><br>
 
 <p>
-For sites that run a large number of <code><VirtualHost></code>s for
-<code>proftpd</code>, it can be cumbersome to configure them all in
-the <code>proftpd.conf</code> file.  Adding or removing virtual server
-configurations require restarting the daemon, as do changes to one of the
-server configurations.  The daemon also consumes memory for each server
+For sites that run a large number of <code><VirtualHost></code>
+sections for ProFTPD, it can be cumbersome to configure them all in the
+<code>proftpd.conf</code> file.  Adding or removing virtual server
+configurations requires <i>restarting</i> the daemon, as do changes to one of
+the server configurations.  The daemon also consumes memory for each server
 configuration, and the memory footprint for the daemon process can grow
 large for large numbers of servers.
 
 <p>
 The <code>mod_autohost</code> module allows for server configurations to
 be configured in individual files, and for those configuration to be used
-in an "on demand" fashion.  Rather than loading the configurations into
+in an <i>on demand</i> fashion.  Rather than loading the configurations into
 memory when the daemon starts up, the daemon will check the IP address and
 port being contacted by a connecting client, check in the filesystem for
 a <code>mod_autohost</code> configuration file for that address/port,
 dynamically parse the configuration, and insert the configuration into
 the session's process space.  Thus changes to the configuration are
-seen whenever a client connects, without requiring a daemon restart.
-The memory footprint is reduced because <code>proftpd</code>, via
-<code>mod_autohost</code>, only reads and uses the needed configuration.
+seen whenever a client connects, <i>without requiring a daemon restart</i>.
+The memory footprint is reduced because ProFTPD, via <code>mod_autohost</code>,
+only reads and uses the needed configuration.
 
 <p>
 This module is contained in the <code>mod_autohost</code> file for
@@ -44,7 +41,7 @@ instructions are discussed <a href="#Installation">here</a>.
 <p>
 The most current version of <code>mod_autohost</code> can be found at:
 <pre>
-  <a href="http://www.castaglia.org/proftpd/">http://www.castaglia.org/proftpd/</a>
+    <a href="https://github.com/Castaglia/proftpd-mod_autohost">https://github.com/Castaglia/proftpd-mod_autohost</a>
 </pre>
 
 <h2>Author</h2>
@@ -74,23 +71,41 @@ The <code>AutoHostConfig</code> directive specifies the path that
 The given <em>path</em> must be an absolute path, and may contain the
 following variables, which will be interpolated:
 <ul>
-  <li><dt>%0</dt>
-      <dd>the entire IP address</dd>
-
-  <li><dt>%1</dt>
-      <dd>the first octet of an IPv4 address</dd>
-
-  <li><dt>%2</dt>
-      <dd>the second octet of an IPv4 address</dd>
-
-  <li><dt>%3</dt>
-      <dd>the third octet of an IPv4 address</dd>
-
-  <li><dt>%4</dt>
-      <dd>the fourth octet of an IPv4 address</dd>
-
-  <li><dt>%p</dt>
-      <dd>the port number</dd>
+  <li><code>%0</code><br>
+    <p>
+    The entire IP address
+  </li>
+
+  <p>
+  <li><code>%1</code><br>
+    <p>
+    The first octet of an IPv4 address
+  </li>
+
+  <li><code>%2</code><br>
+    <p>
+    The second octet of an IPv4 address
+  </li>
+
+  <li><code>%3</code><br>
+    <p>
+    The third octet of an IPv4 address
+  </li>
+
+  <li><code>%4</code><br>
+    <p>
+    The fourth octet of an IPv4 address
+  </li>
+
+  <li><code>%n</code><br>
+    <p>
+    The <em>name</em> used by the client, either via TLS SNI or FTP <code>HOST</code> command
+  </li>
+
+  <li><code>%p</code><br>
+    <p>
+    The port number
+  </li>
 </ul>
 <b>Note</b>: This directive is <b>required</b> for <code>mod_autohost</code>
 to function.
@@ -106,6 +121,7 @@ and a client connecting to 1.2.3.4, the above <em>path</em> would expand into:
   /etc/ftpd/vhosts/1.2.3.4/autohost.conf
 </pre>
 
+<p>
 Given a <em>path</em> of:
 <pre>
   /etc/ftpd/vhosts/%1/%2/%3/%4/%p/vhost.conf
@@ -128,8 +144,11 @@ would check for the following file:
 <p>
 The <code>AutoHostEngine</code> directive enables or disables the module's
 runtime checks for dynamic server configuration files.  If it is set to
-<em>off</em> this module does no checking. Use this directive to disable the
-module instead of commenting out all <code>mod_autohost</code> directives.
+<em>off</em> this module does no checking.
+
+<p>
+Use this directive to disable the module instead of commenting out all
+<code>mod_autohost</code> directives.
 
 <p>
 <hr>
@@ -201,16 +220,16 @@ file into:
 after unpacking the latest proftpd-1.3 source code.  For including
 <code>mod_autohost</code> as a staticly linked module:
 <pre>
-  ./configure --with-modules=mod_autohost
+  $ ./configure --with-modules=mod_autohost
 </pre>
 Alternatively, <code>mod_autohost</code> could be built as a DSO module:
 <pre>
-  ./configure --enable-dso --with-shared=mod_autohost
+  $ ./configure --enable-dso --with-shared=mod_autohost
 </pre>
 Then follow the usual steps:
 <pre>
-  make
-  make install
+  $ make
+  $ make install
 </pre>
 
 <p>
@@ -257,23 +276,12 @@ The <code>mod_tls</code> module will not be able to properly prompt for
 passphrases for keys in an <code>autohost.conf</code> file on server startup.
 
 <p>
-IPv6 addresses are not currently handled by <code>mod_autohost</code>.
-
-<p>
-<hr><br>
-
-Author: <i>$Author: tj $</i><br>
-Last Updated: <i>$Date: 2009/03/03 19:00:07 $</i><br>
-
-<br><hr>
-
+<hr>
 <font size=2><b><i>
-© Copyright 2004-2009 TJ Saunders<br>
+© Copyright 2004-2020 TJ Saunders<br>
  All Rights Reserved<br>
 </i></b></font>
-
-<hr><br>
+<hr>
 
 </body>
 </html>
-


=====================================
t/lib/ProFTPD/Tests/Modules/mod_autohost.pm
=====================================
@@ -9,6 +9,7 @@ use Digest::MD5;
 use File::Path qw(mkpath);
 use File::Spec;
 use IO::Handle;
+use IO::Socket::INET6;
 use POSIX qw(:fcntl_h);
 
 use ProFTPD::TestSuite::FTP;
@@ -24,6 +25,11 @@ my $TESTS = {
     test_class => [qw(forking)],
   },
 
+  autohost_config_ipv6 => {
+    order => ++$order,
+    test_class => [qw(features_ipv6 forking)],
+  },
+
   autohost_ports => {
     order => ++$order,
     test_class => [qw(forking)],
@@ -34,6 +40,14 @@ my $TESTS = {
     test_class => [qw(bug forking)],
   },
 
+  # XXX mod_autohost does not picking up <Global> sections from proftpd.conf
+  # yet; requires a fair amount of reworking, since fixup_globals() removes
+  # the <Global> sections from the config tree.
+  autohost_global_config => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
 };
 
 sub new {
@@ -173,6 +187,155 @@ EOC
   unlink($log_file);
 }
 
+sub autohost_config_ipv6 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'autohost');
+
+  mkpath("$tmpdir/conf.d");
+  my $auto_config = File::Spec->rel2abs("$tmpdir/conf.d/::1.conf");
+  if (open(my $fh, "> $auto_config")) {
+    print $fh <<EOC;
+ServerName "AutoHost IPv6 Server"
+AuthUserFile $setup->{auth_user_file}
+AuthGroupFile $setup->{auth_group_file}
+ServerLog $setup->{log_file}
+RequireValidShell off
+EOC
+    unless (close($fh)) {
+      die("Can't write $auto_config: $!");
+    }
+
+  } else {
+    die("Can't open $auto_config: $!");
+  }
+
+  my $test_root = File::Spec->rel2abs($tmpdir);
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'binding:10 autohost:20',
+
+    UseIPv6 => 'on',
+    DefaultAddress => '::1',
+
+    IfModules => {
+      'mod_autohost.c' => {
+        AutoHostEngine => 'on',
+        AutoHostLog => $setup->{log_file},
+        AutoHostConfig => "$test_root/conf.d/%0.conf",
+      },
+
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($setup->{config_file},
+    $config);
+
+  # Open pipes, for use between the parent and child processes.  Specifically,
+  # the child will indicate when it's done with its test by writing a message
+  # to the parent.
+  my ($rfh, $wfh);
+  unless (pipe($rfh, $wfh)) {
+    die("Can't open pipe: $!");
+  }
+
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      sleep(1);
+
+      my $client = IO::Socket::INET6->new(
+        PeerAddr => '::1',
+        PeerPort => $port,
+        Proto => 'tcp',
+        Timeout => 5,
+        Blocking => 1,
+      );
+      unless ($client) {
+        die("Can't connect to ::1: $!");
+      }
+
+      # Read the banner
+      my $banner = <$client>;
+      chomp($banner);
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "# <<< $banner\n";
+      }
+
+      # Send the USER command
+      my $cmd = "USER $setup->{user}";
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "# >>> $cmd\n";
+      }
+      $client->print("$cmd\r\n");
+      $client->flush();
+
+      # Read USER response
+      my $resp = <$client>;
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "# <<< $resp";
+      }
+
+      my $expected = "331 Password required for $setup->{user}\r\n";
+      $self->assert($expected eq $resp,
+        test_msg("Expected response '$expected', got '$resp'"));
+
+      # Send the PASS command
+      $cmd = "PASS $setup->{passwd}";
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "# >>> PASS ******\r\n";
+      }
+      $client->print("$cmd\r\n");
+      $client->flush();
+
+      # Read PASS response
+      $resp = <$client>;
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "<<< $resp";
+      }
+
+      $expected = "230 User $setup->{user} logged in\r\n";
+      $self->assert($expected eq $resp,
+        test_msg("Expected response '$expected', got '$resp'"));
+
+      $client->close();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($setup->{pid_file});
+  $self->assert_child_ok($pid);
+
+  test_cleanup($setup->{log_file}, $ex);
+}
+
 sub autohost_ports {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
@@ -316,6 +479,140 @@ EOC
 sub autohost_extlog_var_p {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'autohost');
+
+  my $test_root = File::Spec->rel2abs($tmpdir);
+  my $ext_log = File::Spec->rel2abs("$tmpdir/custom.log");
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'DEFAULT:10',
+
+    LogFormat => 'custom "%p"',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($setup->{config_file},
+    $config);
+
+  if (open(my $fh, ">> $setup->{config_file}")) {
+    print $fh <<EOC;
+<IfModule mod_autohost.c>
+  AutoHostEngine on
+  AutoHostLog $setup->{log_file}
+  AutoHostConfig $test_root/conf.d/%0:%p.conf
+  AutoHostPorts $port
+</IfModule>
+EOC
+    unless (close($fh)) {
+      die("Can't write $setup->{config_file}: $!");
+    }
+
+  } else {
+    die("Can't open $setup->{config_file}: $!");
+  }
+
+  mkpath("$tmpdir/conf.d");
+  my $auto_config = File::Spec->rel2abs("$tmpdir/conf.d/127.0.0.1:$port.conf");
+  if (open(my $fh, "> $auto_config")) {
+    print $fh <<EOC;
+ServerName "AutoHost Server"
+AuthUserFile $setup->{auth_user_file}
+AuthGroupFile $setup->{auth_group_file}
+ServerLog $setup->{log_file}
+RequireValidShell off
+ExtendedLog $ext_log ALL custom
+EOC
+    unless (close($fh)) {
+      die("Can't write $auto_config: $!");
+    }
+
+  } else {
+    die("Can't open $auto_config: $!");
+  }
+
+  # Open pipes, for use between the parent and child processes.  Specifically,
+  # the child will indicate when it's done with its test by writing a message
+  # to the parent.
+  my ($rfh, $wfh);
+  unless (pipe($rfh, $wfh)) {
+    die("Can't open pipe: $!");
+  }
+
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->quit();
+    };
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($setup->{pid_file});
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_cleanup($setup->{log_file}, $ex);
+    return;
+  }
+
+  # Now, read in the ExtendedLog, and see whether the %p variable was
+  # properly written out.
+  eval {
+    if (open(my $fh, "< $ext_log")) {
+      my $line = <$fh>;
+      chomp($line);
+      close($fh);
+
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "# $line\n";
+      }
+
+      $self->assert($port eq $line,
+        test_msg("Expected '$port', got '$line'"));
+
+    } else {
+      die("Can't read $ext_log: $!");
+    }
+  };
+  if ($@) {
+    $ex = $@;
+  }
+
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+sub autohost_global_config {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
 
   my $config_file = "$tmpdir/autohost.conf";
   my $pid_file = File::Spec->rel2abs("$tmpdir/autohost.pid");
@@ -367,6 +664,10 @@ sub autohost_extlog_var_p {
         DelayEngine => 'off',
       },
     },
+
+    Global => {
+      ExtendedLog => "$ext_log ALL custom",
+    },
   };
 
   my ($port, $config_user, $config_group) = config_write($config_file, $config);
@@ -397,7 +698,6 @@ AuthUserFile $auth_user_file
 AuthGroupFile $auth_group_file
 ServerLog $log_file
 RequireValidShell off
-ExtendedLog $ext_log ALL custom
 EOC
     unless (close($fh)) {
       die("Can't write $auto_config: $!");


=====================================
t/lib/ProFTPD/Tests/Modules/mod_autohost/host.pm
=====================================
@@ -0,0 +1,560 @@
+package ProFTPD::Tests::Modules::mod_autohost::host;
+
+use lib qw(t/lib);
+use base qw(ProFTPD::TestSuite::Child);
+use strict;
+
+use Cwd;
+use Digest::MD5;
+use File::Path qw(mkpath);
+use File::Spec;
+use IO::Handle;
+use IO::Socket::INET6;
+use POSIX qw(:fcntl_h);
+
+use ProFTPD::TestSuite::FTP;
+use ProFTPD::TestSuite::Utils qw(:auth :config :running :test :testsuite);
+
+$| = 1;
+
+my $order = 0;
+
+my $TESTS = {
+  autohost_host_config_ok => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  autohost_host_config_missing => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  autohost_host_config_no_serveralias => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  autohost_host_config_mismatched_serveralias => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  autohost_host_config_existing_serveralias => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+};
+
+sub new {
+  return shift()->SUPER::new(@_);
+}
+
+sub list_tests {
+  return testsuite_get_runnable_tests($TESTS);
+}
+
+sub autohost_host_config_ok {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'autohost');
+
+  my $host = 'castaglia';
+
+  mkpath("$tmpdir/conf.d");
+  my $auto_config = File::Spec->rel2abs("$tmpdir/conf.d/127.0.0.1-$host.conf");
+  if (open(my $fh, "> $auto_config")) {
+    print $fh <<EOC;
+ServerAlias $host
+ServerName "AutoHost Server"
+AuthUserFile $setup->{auth_user_file}
+AuthGroupFile $setup->{auth_group_file}
+ServerLog $setup->{log_file}
+RequireValidShell off
+EOC
+    unless (close($fh)) {
+      die("Can't write $auto_config: $!");
+    }
+
+  } else {
+    die("Can't open $auto_config: $!");
+  }
+
+  my $test_root = File::Spec->rel2abs($tmpdir);
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'autohost:20 binding:20 event:20',
+
+    IfModules => {
+      'mod_autohost.c' => {
+        AutoHostEngine => 'on',
+        AutoHostLog => $setup->{log_file},
+        AutoHostConfig => "$test_root/conf.d/%0-%n.conf",
+      },
+
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($setup->{config_file},
+    $config);
+
+  # Open pipes, for use between the parent and child processes.  Specifically,
+  # the child will indicate when it's done with its test by writing a message
+  # to the parent.
+  my ($rfh, $wfh);
+  unless (pipe($rfh, $wfh)) {
+    die("Can't open pipe: $!");
+  }
+
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      $client->host($host);
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected = 'AutoHost Server';
+      $self->assert(qr/$expected/, $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->quit();
+    };
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($setup->{pid_file});
+  $self->assert_child_ok($pid);
+
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+sub autohost_host_config_missing {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'autohost');
+
+  my $host = 'castaglia';
+
+  mkpath("$tmpdir/conf.d");
+  my $auto_config = File::Spec->rel2abs("$tmpdir/conf.d/127.0.0.1-foo.conf");
+  if (open(my $fh, "> $auto_config")) {
+    print $fh <<EOC;
+ServerAlias $host
+ServerName "AutoHost Server"
+AuthUserFile $setup->{auth_user_file}
+AuthGroupFile $setup->{auth_group_file}
+ServerLog $setup->{log_file}
+RequireValidShell off
+EOC
+    unless (close($fh)) {
+      die("Can't write $auto_config: $!");
+    }
+
+  } else {
+    die("Can't open $auto_config: $!");
+  }
+
+  my $test_root = File::Spec->rel2abs($tmpdir);
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'autohost:20 binding:20 event:20',
+
+    IfModules => {
+      'mod_autohost.c' => {
+        AutoHostEngine => 'on',
+        AutoHostLog => $setup->{log_file},
+        AutoHostConfig => "$test_root/conf.d/%0-%n.conf",
+      },
+
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($setup->{config_file},
+    $config);
+
+  # Open pipes, for use between the parent and child processes.  Specifically,
+  # the child will indicate when it's done with its test by writing a message
+  # to the parent.
+  my ($rfh, $wfh);
+  unless (pipe($rfh, $wfh)) {
+    die("Can't open pipe: $!");
+  }
+
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      eval { $client->host($host) };
+      unless ($@) {
+        die("HOST $host succeeded unexpectedly");
+      }
+
+      $client->quit();
+    };
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($setup->{pid_file});
+  $self->assert_child_ok($pid);
+
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+sub autohost_host_config_no_serveralias {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'autohost');
+
+  my $host = 'castaglia';
+
+  mkpath("$tmpdir/conf.d");
+  my $auto_config = File::Spec->rel2abs("$tmpdir/conf.d/127.0.0.1-$host.conf");
+  if (open(my $fh, "> $auto_config")) {
+    print $fh <<EOC;
+ServerName "AutoHost Server"
+AuthUserFile $setup->{auth_user_file}
+AuthGroupFile $setup->{auth_group_file}
+ServerLog $setup->{log_file}
+RequireValidShell off
+EOC
+    unless (close($fh)) {
+      die("Can't write $auto_config: $!");
+    }
+
+  } else {
+    die("Can't open $auto_config: $!");
+  }
+
+  my $test_root = File::Spec->rel2abs($tmpdir);
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'autohost:20 binding:20 event:20',
+
+    IfModules => {
+      'mod_autohost.c' => {
+        AutoHostEngine => 'on',
+        AutoHostLog => $setup->{log_file},
+        AutoHostConfig => "$test_root/conf.d/%0-%n.conf",
+      },
+
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($setup->{config_file},
+    $config);
+
+  # Open pipes, for use between the parent and child processes.  Specifically,
+  # the child will indicate when it's done with its test by writing a message
+  # to the parent.
+  my ($rfh, $wfh);
+  unless (pipe($rfh, $wfh)) {
+    die("Can't open pipe: $!");
+  }
+
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      eval { $client->host($host) };
+      unless ($@) {
+        die("HOST $host succeeded unexpectedly");
+      }
+
+      $client->quit();
+    };
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($setup->{pid_file});
+  $self->assert_child_ok($pid);
+
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+sub autohost_host_config_mismatched_serveralias {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'autohost');
+
+  my $host = 'castaglia';
+
+  mkpath("$tmpdir/conf.d");
+  my $auto_config = File::Spec->rel2abs("$tmpdir/conf.d/127.0.0.1-$host.conf");
+  if (open(my $fh, "> $auto_config")) {
+    print $fh <<EOC;
+ServerAlias ftp.example.com
+ServerName "AutoHost Server"
+AuthUserFile $setup->{auth_user_file}
+AuthGroupFile $setup->{auth_group_file}
+ServerLog $setup->{log_file}
+RequireValidShell off
+EOC
+    unless (close($fh)) {
+      die("Can't write $auto_config: $!");
+    }
+
+  } else {
+    die("Can't open $auto_config: $!");
+  }
+
+  my $test_root = File::Spec->rel2abs($tmpdir);
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'autohost:20 binding:20 event:20',
+
+    IfModules => {
+      'mod_autohost.c' => {
+        AutoHostEngine => 'on',
+        AutoHostLog => $setup->{log_file},
+        AutoHostConfig => "$test_root/conf.d/%0-%n.conf",
+      },
+
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($setup->{config_file},
+    $config);
+
+  # Open pipes, for use between the parent and child processes.  Specifically,
+  # the child will indicate when it's done with its test by writing a message
+  # to the parent.
+  my ($rfh, $wfh);
+  unless (pipe($rfh, $wfh)) {
+    die("Can't open pipe: $!");
+  }
+
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      eval { $client->host($host) };
+      unless ($@) {
+        die("HOST $host succeeded unexpectedly");
+      }
+
+      $client->quit();
+    };
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($setup->{pid_file});
+  $self->assert_child_ok($pid);
+
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+sub autohost_host_config_existing_serveralias {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'autohost');
+
+  my $host = 'castaglia';
+
+  mkpath("$tmpdir/conf.d");
+  my $auto_config = File::Spec->rel2abs("$tmpdir/conf.d/127.0.0.1-$host.conf");
+  if (open(my $fh, "> $auto_config")) {
+    print $fh <<EOC;
+ServerAlias $host
+ServerName "AutoHost Server"
+AuthUserFile $setup->{auth_user_file}
+AuthGroupFile $setup->{auth_group_file}
+ServerLog $setup->{log_file}
+RequireValidShell off
+EOC
+    unless (close($fh)) {
+      die("Can't write $auto_config: $!");
+    }
+
+  } else {
+    die("Can't open $auto_config: $!");
+  }
+
+  my $test_root = File::Spec->rel2abs($tmpdir);
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'autohost:20 binding:20 event:20',
+
+    ServerAlias => $host,
+    ServerName => '"Default Server"',
+
+    IfModules => {
+      'mod_autohost.c' => {
+        AutoHostEngine => 'on',
+        AutoHostLog => $setup->{log_file},
+        AutoHostConfig => "$test_root/conf.d/%0-%n.conf",
+      },
+
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($setup->{config_file},
+    $config);
+
+  # Open pipes, for use between the parent and child processes.  Specifically,
+  # the child will indicate when it's done with its test by writing a message
+  # to the parent.
+  my ($rfh, $wfh);
+  unless (pipe($rfh, $wfh)) {
+    die("Can't open pipe: $!");
+  }
+
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      $client->host($host);
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected = 'AutoHost Server';
+      $self->assert(qr/$expected/, $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->quit();
+    };
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($setup->{pid_file});
+  $self->assert_child_ok($pid);
+
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+1;


=====================================
t/lib/ProFTPD/Tests/Modules/mod_autohost/sni.pm
=====================================
@@ -0,0 +1,935 @@
+package ProFTPD::Tests::Modules::mod_autohost::sni;
+
+use lib qw(t/lib);
+use base qw(ProFTPD::TestSuite::Child);
+use strict;
+
+use Cwd;
+use Digest::MD5;
+use File::Path qw(mkpath);
+use File::Spec;
+use IO::Handle;
+use IO::Socket::INET6;
+use POSIX qw(:fcntl_h);
+
+use ProFTPD::TestSuite::FTP;
+use ProFTPD::TestSuite::Utils qw(:auth :config :running :test :testsuite);
+
+$| = 1;
+
+my $order = 0;
+
+my $TESTS = {
+  autohost_sni_config_ok => {
+    order => ++$order,
+    test_class => [qw(forking mod_tls)],
+  },
+
+  autohost_sni_config_missing => {
+    order => ++$order,
+    test_class => [qw(forking mod_tls)],
+  },
+
+  autohost_sni_config_no_serveralias => {
+    order => ++$order,
+    test_class => [qw(forking mod_tls)],
+  },
+
+  autohost_sni_config_mismatched_serveralias => {
+    order => ++$order,
+    test_class => [qw(forking mod_tls)],
+  },
+
+  autohost_sni_config_existing_serveralias => {
+    order => ++$order,
+    test_class => [qw(forking mod_tls)],
+  },
+
+  autohost_sni_config_ok_with_host => {
+    order => ++$order,
+    test_class => [qw(forking mod_tls)],
+  },
+
+};
+
+sub new {
+  return shift()->SUPER::new(@_);
+}
+
+sub list_tests {
+  # Check for the required Perl modules:
+  #
+  #  Net-SSLeay
+  #  IO-Socket-SSL
+  #  Net-FTPSSL
+
+  my $required = [qw(
+    Net::SSLeay
+    IO::Socket::SSL
+    Net::FTPSSL
+  )];
+
+  foreach my $req (@$required) {
+    eval "use $req";
+    if ($@) {
+      print STDERR "\nWARNING:\n + Module '$req' not found, skipping all tests\n";
+
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "Unable to load $req: $@\n";
+      }
+
+      return qw(testsuite_empty_test);
+    }
+  }
+
+  return testsuite_get_runnable_tests($TESTS);
+}
+
+sub autohost_sni_config_ok {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'autohost');
+
+  my $host = 'castaglia';
+
+  my $cert_file = File::Spec->rel2abs("$ENV{PROFTPD_TEST_DIR}/t/etc/modules/mod_tls/server-cert.pem");
+  my $ca_file = File::Spec->rel2abs("$ENV{PROFTPD_TEST_DIR}/t/etc/modules/mod_tls/ca-cert.pem");
+
+  mkpath("$tmpdir/conf.d");
+  my $auto_config = File::Spec->rel2abs("$tmpdir/conf.d/127.0.0.1-$host.conf");
+  if (open(my $fh, "> $auto_config")) {
+    print $fh <<EOC;
+ServerAlias $host
+ServerName "AutoHost Server"
+AuthUserFile $setup->{auth_user_file}
+AuthGroupFile $setup->{auth_group_file}
+ServerLog $setup->{log_file}
+RequireValidShell off
+
+<IfModule mod_tls.c>
+  TLSEngine on
+  TLSLog $setup->{log_file}
+  TLSRequired on
+  TLSRSACertificateFile $cert_file
+  TLSCACertificateFile $ca_file
+  TLSOptions EnableDiags
+</IfModule>
+EOC
+    unless (close($fh)) {
+      die("Can't write $auto_config: $!");
+    }
+
+  } else {
+    die("Can't open $auto_config: $!");
+  }
+
+  my $test_root = File::Spec->rel2abs($tmpdir);
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'autohost:20 binding:20 event:20 tls:20',
+
+    IfModules => {
+      'mod_autohost.c' => {
+        AutoHostEngine => 'on',
+        AutoHostLog => $setup->{log_file},
+        AutoHostConfig => "$test_root/conf.d/%0-%n.conf",
+      },
+
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_tls.c' => {
+        TLSEngine => 'on',
+        TLSLog => $setup->{log_file},
+        TLSRequired => 'on',
+        TLSRSACertificateFile => $cert_file,
+        TLSCACertificateFile => $ca_file,
+        TLSOptions => 'EnableDiags',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($setup->{config_file},
+    $config);
+
+  # Open pipes, for use between the parent and child processes.  Specifically,
+  # the child will indicate when it's done with its test by writing a message
+  # to the parent.
+  my ($rfh, $wfh);
+  unless (pipe($rfh, $wfh)) {
+    die("Can't open pipe: $!");
+  }
+
+  require Net::FTPSSL;
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      # Give the server a chance to start up
+      sleep(2);
+
+      my $ssl_opts = {
+        Encryption => 'E',
+        Port => $port,
+        SSL_ca_file => $ca_file,
+        SSL_hostname => $host,
+        SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_PEER(),
+      };
+
+      if ($ENV{TEST_VERBOSE}) {
+        $ssl_opts->{Debug} = 2;
+      }
+
+      my $client = Net::FTPSSL->new('127.0.0.1', $ssl_opts);
+      unless ($client) {
+        die("Can't connect to FTPS server: " . IO::Socket::SSL::errstr());
+      }
+
+      # Our default server will not have the AuthUserFile configured with our
+      # test user; only the name-based AutoHost config does.  So if login
+      # succeeds, we will know that that name-based AutoHost config has been
+      # loaded/used successfully.
+      unless ($client->login($setup->{user}, $setup->{passwd})) {
+        die("Login failed unexpectedly: ". $client->last_message());
+      }
+
+      $client->quit();
+    };
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($setup->{pid_file});
+  $self->assert_child_ok($pid);
+
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+sub autohost_sni_config_missing {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'autohost');
+
+  my $host = 'castaglia';
+
+  my $cert_file = File::Spec->rel2abs("$ENV{PROFTPD_TEST_DIR}/t/etc/modules/mod_tls/server-cert.pem");
+  my $ca_file = File::Spec->rel2abs("$ENV{PROFTPD_TEST_DIR}/t/etc/modules/mod_tls/ca-cert.pem");
+
+  mkpath("$tmpdir/conf.d");
+  my $auto_config = File::Spec->rel2abs("$tmpdir/conf.d/127.0.0.1-foo.conf");
+  if (open(my $fh, "> $auto_config")) {
+    print $fh <<EOC;
+ServerAlias $host
+ServerName "AutoHost Server"
+AuthUserFile $setup->{auth_user_file}
+AuthGroupFile $setup->{auth_group_file}
+ServerLog $setup->{log_file}
+RequireValidShell off
+
+<IfModule mod_tls.c>
+  TLSEngine on
+  TLSLog $setup->{log_file}
+  TLSRequired on
+  TLSRSACertificateFile $cert_file
+  TLSCACertificateFile $ca_file
+  TLSOptions EnableDiags
+</IfModule>
+EOC
+    unless (close($fh)) {
+      die("Can't write $auto_config: $!");
+    }
+
+  } else {
+    die("Can't open $auto_config: $!");
+  }
+
+  my $test_root = File::Spec->rel2abs($tmpdir);
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'autohost:20 binding:20 event:20 tls:20',
+
+    IfModules => {
+      'mod_autohost.c' => {
+        AutoHostEngine => 'on',
+        AutoHostLog => $setup->{log_file},
+        AutoHostConfig => "$test_root/conf.d/%0-%n.conf",
+      },
+
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_tls.c' => {
+        TLSEngine => 'on',
+        TLSLog => $setup->{log_file},
+        TLSRequired => 'on',
+        TLSRSACertificateFile => $cert_file,
+        TLSCACertificateFile => $ca_file,
+        TLSOptions => 'EnableDiags',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($setup->{config_file},
+    $config);
+
+  # Open pipes, for use between the parent and child processes.  Specifically,
+  # the child will indicate when it's done with its test by writing a message
+  # to the parent.
+  my ($rfh, $wfh);
+  unless (pipe($rfh, $wfh)) {
+    die("Can't open pipe: $!");
+  }
+
+  require Net::FTPSSL;
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      # Give the server a chance to start up
+      sleep(2);
+
+      my $ssl_opts = {
+        Encryption => 'E',
+        Port => $port,
+        SSL_ca_file => $ca_file,
+        SSL_hostname => $host,
+        SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_PEER(),
+      };
+
+      if ($ENV{TEST_VERBOSE}) {
+        $ssl_opts->{Debug} = 2;
+      }
+
+      my $client = Net::FTPSSL->new('127.0.0.1', $ssl_opts);
+      if ($client) {
+        die("Connected to FTPS server unexpectedly");
+      }
+
+      my $errstr = IO::Socket::SSL::errstr();
+      my $expected = 'handshake failure|unrecognized name';
+      $self->assert(qr/$expected/, $errstr,
+        test_msg("Expected '$expected', got '$errstr'"));
+    };
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($setup->{pid_file});
+  $self->assert_child_ok($pid);
+
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+sub autohost_sni_config_no_serveralias {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'autohost');
+
+  my $host = 'castaglia';
+
+  my $cert_file = File::Spec->rel2abs("$ENV{PROFTPD_TEST_DIR}/t/etc/modules/mod_tls/server-cert.pem");
+  my $ca_file = File::Spec->rel2abs("$ENV{PROFTPD_TEST_DIR}/t/etc/modules/mod_tls/ca-cert.pem");
+
+  mkpath("$tmpdir/conf.d");
+  my $auto_config = File::Spec->rel2abs("$tmpdir/conf.d/127.0.0.1-$host.conf");
+  if (open(my $fh, "> $auto_config")) {
+    print $fh <<EOC;
+ServerName "AutoHost Server"
+AuthUserFile $setup->{auth_user_file}
+AuthGroupFile $setup->{auth_group_file}
+ServerLog $setup->{log_file}
+RequireValidShell off
+
+<IfModule mod_tls.c>
+  TLSEngine on
+  TLSLog $setup->{log_file}
+  TLSRequired on
+  TLSRSACertificateFile $cert_file
+  TLSCACertificateFile $ca_file
+  TLSOptions EnableDiags
+</IfModule>
+EOC
+    unless (close($fh)) {
+      die("Can't write $auto_config: $!");
+    }
+
+  } else {
+    die("Can't open $auto_config: $!");
+  }
+
+  my $test_root = File::Spec->rel2abs($tmpdir);
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'autohost:20 binding:20 event:20 tls:20',
+
+    IfModules => {
+      'mod_autohost.c' => {
+        AutoHostEngine => 'on',
+        AutoHostLog => $setup->{log_file},
+        AutoHostConfig => "$test_root/conf.d/%0-%n.conf",
+      },
+
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_tls.c' => {
+        TLSEngine => 'on',
+        TLSLog => $setup->{log_file},
+        TLSRequired => 'on',
+        TLSRSACertificateFile => $cert_file,
+        TLSCACertificateFile => $ca_file,
+        TLSOptions => 'EnableDiags',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($setup->{config_file},
+    $config);
+
+  # Open pipes, for use between the parent and child processes.  Specifically,
+  # the child will indicate when it's done with its test by writing a message
+  # to the parent.
+  my ($rfh, $wfh);
+  unless (pipe($rfh, $wfh)) {
+    die("Can't open pipe: $!");
+  }
+
+  require Net::FTPSSL;
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      # Give the server a chance to start up
+      sleep(2);
+
+      my $ssl_opts = {
+        Encryption => 'E',
+        Port => $port,
+        SSL_ca_file => $ca_file,
+        SSL_hostname => $host,
+        SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_PEER(),
+      };
+
+      if ($ENV{TEST_VERBOSE}) {
+        $ssl_opts->{Debug} = 2;
+      }
+
+      my $client = Net::FTPSSL->new('127.0.0.1', $ssl_opts);
+      if ($client) {
+        die("Connected to FTPS server unexpectedly");
+      }
+
+      my $errstr = IO::Socket::SSL::errstr();
+      my $expected = 'handshake failure|unrecognized name';
+      $self->assert(qr/$expected/, $errstr,
+        test_msg("Expected '$expected', got '$errstr'"));
+    };
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($setup->{pid_file});
+  $self->assert_child_ok($pid);
+
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+sub autohost_sni_config_mismatched_serveralias {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'autohost');
+
+  my $host = 'castaglia';
+
+  my $cert_file = File::Spec->rel2abs("$ENV{PROFTPD_TEST_DIR}/t/etc/modules/mod_tls/server-cert.pem");
+  my $ca_file = File::Spec->rel2abs("$ENV{PROFTPD_TEST_DIR}/t/etc/modules/mod_tls/ca-cert.pem");
+
+  mkpath("$tmpdir/conf.d");
+  my $auto_config = File::Spec->rel2abs("$tmpdir/conf.d/127.0.0.1-$host.conf");
+  if (open(my $fh, "> $auto_config")) {
+    print $fh <<EOC;
+ServerAlias ftp.example.com
+ServerName "AutoHost Server"
+AuthUserFile $setup->{auth_user_file}
+AuthGroupFile $setup->{auth_group_file}
+ServerLog $setup->{log_file}
+RequireValidShell off
+
+<IfModule mod_tls.c>
+  TLSEngine on
+  TLSLog $setup->{log_file}
+  TLSRequired on
+  TLSRSACertificateFile $cert_file
+  TLSCACertificateFile $ca_file
+  TLSOptions EnableDiags
+</IfModule>
+EOC
+    unless (close($fh)) {
+      die("Can't write $auto_config: $!");
+    }
+
+  } else {
+    die("Can't open $auto_config: $!");
+  }
+
+  my $test_root = File::Spec->rel2abs($tmpdir);
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'autohost:20 binding:20 event:20 tls:20',
+
+    IfModules => {
+      'mod_autohost.c' => {
+        AutoHostEngine => 'on',
+        AutoHostLog => $setup->{log_file},
+        AutoHostConfig => "$test_root/conf.d/%0-%n.conf",
+      },
+
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_tls.c' => {
+        TLSEngine => 'on',
+        TLSLog => $setup->{log_file},
+        TLSRequired => 'on',
+        TLSRSACertificateFile => $cert_file,
+        TLSCACertificateFile => $ca_file,
+        TLSOptions => 'EnableDiags',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($setup->{config_file},
+    $config);
+
+  # Open pipes, for use between the parent and child processes.  Specifically,
+  # the child will indicate when it's done with its test by writing a message
+  # to the parent.
+  my ($rfh, $wfh);
+  unless (pipe($rfh, $wfh)) {
+    die("Can't open pipe: $!");
+  }
+
+  require Net::FTPSSL;
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      # Give the server a chance to start up
+      sleep(2);
+
+      my $ssl_opts = {
+        Encryption => 'E',
+        Port => $port,
+        SSL_ca_file => $ca_file,
+        SSL_hostname => $host,
+        SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_PEER(),
+      };
+
+      if ($ENV{TEST_VERBOSE}) {
+        $ssl_opts->{Debug} = 2;
+      }
+
+      my $client = Net::FTPSSL->new('127.0.0.1', $ssl_opts);
+      if ($client) {
+        die("Connected to FTPS server unexpectedly");
+      }
+
+      my $errstr = IO::Socket::SSL::errstr();
+      my $expected = 'handshake failure|unrecognized name';
+      $self->assert(qr/$expected/, $errstr,
+        test_msg("Expected '$expected', got '$errstr'"));
+    };
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($setup->{pid_file});
+  $self->assert_child_ok($pid);
+
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+sub autohost_sni_config_existing_serveralias {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'autohost');
+
+  my $host = 'castaglia';
+
+  my $cert_file = File::Spec->rel2abs("$ENV{PROFTPD_TEST_DIR}/t/etc/modules/mod_tls/server-cert.pem");
+  my $ca_file = File::Spec->rel2abs("$ENV{PROFTPD_TEST_DIR}/t/etc/modules/mod_tls/ca-cert.pem");
+
+  mkpath("$tmpdir/conf.d");
+  my $auto_config = File::Spec->rel2abs("$tmpdir/conf.d/127.0.0.1-$host.conf");
+  if (open(my $fh, "> $auto_config")) {
+    print $fh <<EOC;
+ServerAlias $host
+ServerName "AutoHost Server"
+AuthUserFile $setup->{auth_user_file}
+AuthGroupFile $setup->{auth_group_file}
+ServerLog $setup->{log_file}
+RequireValidShell off
+
+<IfModule mod_tls.c>
+  TLSEngine on
+  TLSLog $setup->{log_file}
+  TLSRequired on
+  TLSRSACertificateFile $cert_file
+  TLSCACertificateFile $ca_file
+  TLSOptions EnableDiags
+</IfModule>
+EOC
+    unless (close($fh)) {
+      die("Can't write $auto_config: $!");
+    }
+
+  } else {
+    die("Can't open $auto_config: $!");
+  }
+
+  my $test_root = File::Spec->rel2abs($tmpdir);
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'autohost:20 binding:20 event:20 tls:20',
+
+    ServerAlias => $host,
+
+    IfModules => {
+      'mod_autohost.c' => {
+        AutoHostEngine => 'on',
+        AutoHostLog => $setup->{log_file},
+        AutoHostConfig => "$test_root/conf.d/%0-%n.conf",
+      },
+
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_tls.c' => {
+        TLSEngine => 'on',
+        TLSLog => $setup->{log_file},
+        TLSRequired => 'on',
+        TLSRSACertificateFile => $cert_file,
+        TLSCACertificateFile => $ca_file,
+        TLSOptions => 'EnableDiags',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($setup->{config_file},
+    $config);
+
+  # Open pipes, for use between the parent and child processes.  Specifically,
+  # the child will indicate when it's done with its test by writing a message
+  # to the parent.
+  my ($rfh, $wfh);
+  unless (pipe($rfh, $wfh)) {
+    die("Can't open pipe: $!");
+  }
+
+  require Net::FTPSSL;
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      # Give the server a chance to start up
+      sleep(2);
+
+      my $ssl_opts = {
+        Encryption => 'E',
+        Port => $port,
+        SSL_ca_file => $ca_file,
+        SSL_hostname => $host,
+        SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_PEER(),
+      };
+
+      if ($ENV{TEST_VERBOSE}) {
+        $ssl_opts->{Debug} = 2;
+      }
+
+      my $client = Net::FTPSSL->new('127.0.0.1', $ssl_opts);
+      unless ($client) {
+        die("Can't connect to FTPS server: " . IO::Socket::SSL::errstr());
+      }
+
+      # Our default server will not have the AuthUserFile configured with our
+      # test user; only the name-based AutoHost config does.  So if login
+      # succeeds, we will know that that name-based AutoHost config has been
+      # loaded/used successfully.
+      unless ($client->login($setup->{user}, $setup->{passwd})) {
+        die("Login failed unexpectedly: ". $client->last_message());
+      }
+
+      $client->quit();
+    };
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($setup->{pid_file});
+  $self->assert_child_ok($pid);
+
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+sub autohost_sni_config_ok_with_host {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'autohost');
+
+  my $host = 'castaglia';
+
+  my $cert_file = File::Spec->rel2abs("$ENV{PROFTPD_TEST_DIR}/t/etc/modules/mod_tls/server-cert.pem");
+  my $ca_file = File::Spec->rel2abs("$ENV{PROFTPD_TEST_DIR}/t/etc/modules/mod_tls/ca-cert.pem");
+
+  mkpath("$tmpdir/conf.d");
+  my $auto_config = File::Spec->rel2abs("$tmpdir/conf.d/127.0.0.1-$host.conf");
+  if (open(my $fh, "> $auto_config")) {
+    print $fh <<EOC;
+ServerAlias $host
+ServerName "AutoHost Server"
+AuthUserFile $setup->{auth_user_file}
+AuthGroupFile $setup->{auth_group_file}
+ServerLog $setup->{log_file}
+RequireValidShell off
+
+<IfModule mod_tls.c>
+  TLSEngine on
+  TLSLog $setup->{log_file}
+  TLSRequired on
+  TLSRSACertificateFile $cert_file
+  TLSCACertificateFile $ca_file
+  TLSOptions EnableDiags
+</IfModule>
+EOC
+    unless (close($fh)) {
+      die("Can't write $auto_config: $!");
+    }
+
+  } else {
+    die("Can't open $auto_config: $!");
+  }
+
+  my $test_root = File::Spec->rel2abs($tmpdir);
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'autohost:20 binding:20 event:20 tls:20',
+
+    IfModules => {
+      'mod_autohost.c' => {
+        AutoHostEngine => 'on',
+        AutoHostLog => $setup->{log_file},
+        AutoHostConfig => "$test_root/conf.d/%0-%n.conf",
+      },
+
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_tls.c' => {
+        TLSEngine => 'on',
+        TLSLog => $setup->{log_file},
+        TLSRequired => 'on',
+        TLSRSACertificateFile => $cert_file,
+        TLSCACertificateFile => $ca_file,
+        TLSOptions => 'EnableDiags',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($setup->{config_file},
+    $config);
+
+  # Open pipes, for use between the parent and child processes.  Specifically,
+  # the child will indicate when it's done with its test by writing a message
+  # to the parent.
+  my ($rfh, $wfh);
+  unless (pipe($rfh, $wfh)) {
+    die("Can't open pipe: $!");
+  }
+
+  require Net::FTPSSL;
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      # Give the server a chance to start up
+      sleep(2);
+
+      my $ssl_opts = {
+        Encryption => 'E',
+        Port => $port,
+        SSL_ca_file => $ca_file,
+        SSL_hostname => $host,
+        SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_PEER(),
+      };
+
+      if ($ENV{TEST_VERBOSE}) {
+        $ssl_opts->{Debug} = 2;
+      }
+
+      my $client = Net::FTPSSL->new('127.0.0.1', $ssl_opts);
+      unless ($client) {
+        die("Can't connect to FTPS server: " . IO::Socket::SSL::errstr());
+      }
+
+      unless ($client->quot('HOST', $host)) {
+        die("HOST failed unexpectedly: ". $client->last_message());
+      }
+
+      my $resp_msg = $client->last_message();
+      my $expected = 'AutoHost Server';
+      $self->assert(qr/$expected/, $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      # Our default server will not have the AuthUserFile configured with our
+      # test user; only the name-based AutoHost config does.  So if login
+      # succeeds, we will know that that name-based AutoHost config has been
+      # loaded/used successfully.
+      unless ($client->login($setup->{user}, $setup->{passwd})) {
+        die("Login failed unexpectedly: ". $client->last_message());
+      }
+
+      $client->quit();
+    };
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($setup->{pid_file});
+  $self->assert_child_ok($pid);
+
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+1;


=====================================
t/modules/mod_autohost/host.t
=====================================
@@ -0,0 +1,11 @@
+#!/usr/bin/env perl
+
+use lib qw(t/lib);
+use strict;
+
+use Test::Unit::HarnessUnit;
+
+$| = 1;
+
+my $r = Test::Unit::HarnessUnit->new();
+$r->start("ProFTPD::Tests::Modules::mod_autohost::host");


=====================================
t/modules/mod_autohost/sni.t
=====================================
@@ -0,0 +1,11 @@
+#!/usr/bin/env perl
+
+use lib qw(t/lib);
+use strict;
+
+use Test::Unit::HarnessUnit;
+
+$| = 1;
+
+my $r = Test::Unit::HarnessUnit->new();
+$r->start("ProFTPD::Tests::Modules::mod_autohost::sni");



View it on GitLab: https://salsa.debian.org/debian-proftpd-team/proftpd-mod-autohost/-/commit/eb22f567865cac9874f0b49e3af262559d53bf6f

-- 
View it on GitLab: https://salsa.debian.org/debian-proftpd-team/proftpd-mod-autohost/-/commit/eb22f567865cac9874f0b49e3af262559d53bf6f
You're receiving this email because of your account on salsa.debian.org.




More information about the Pkg-proftpd-maintainers mailing list